diff --git a/CandyHouseSolution/CandyHouseContracts/Mapper/AlternativeNameAttribute.cs b/CandyHouseSolution/CandyHouseContracts/Mapper/AlternativeNameAttribute.cs new file mode 100644 index 0000000..f3922f3 --- /dev/null +++ b/CandyHouseSolution/CandyHouseContracts/Mapper/AlternativeNameAttribute.cs @@ -0,0 +1,12 @@ +namespace MagicCarpetContracts.Mapper; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)] +public class AlternativeNameAttribute : Attribute +{ + public string AlternativeName { get; } + + public AlternativeNameAttribute(string alternativeName) + { + AlternativeName = alternativeName; + } +} diff --git a/CandyHouseSolution/CandyHouseContracts/Mapper/CustomMapper.cs b/CandyHouseSolution/CandyHouseContracts/Mapper/CustomMapper.cs new file mode 100644 index 0000000..2428429 --- /dev/null +++ b/CandyHouseSolution/CandyHouseContracts/Mapper/CustomMapper.cs @@ -0,0 +1,281 @@ +using System.Collections; +using System.ComponentModel; +using System.Reflection; + +namespace MagicCarpetContracts.Mapper; + +internal static class CustomMapper +{ + public static To MapObject(object obj, To newObject) + { + ArgumentNullException.ThrowIfNull(obj); + ArgumentNullException.ThrowIfNull(newObject); + + var typeFrom = obj.GetType(); + var typeTo = newObject.GetType(); + + var propertiesFrom = typeFrom.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.CanRead) + .ToArray(); + + // свойств + foreach (var property in typeTo.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.CanWrite)) + { + if (property.GetCustomAttribute() is not null) + continue; + + var propertyFrom = TryGetPropertyFrom(property, propertiesFrom); + if (propertyFrom is null) + { + FindAndMapDefaultValue(property, newObject); + continue; + } + + var fromValue = propertyFrom.GetValue(obj); + var postProcessingAttribute = property.GetCustomAttribute(); + if (postProcessingAttribute is not null) + { + var value = PostProcessing(fromValue, postProcessingAttribute, newObject); + if (value is not null) + { + property.SetValue(newObject, value); + } + continue; + } + + if (propertyFrom.PropertyType.IsGenericType && propertyFrom.PropertyType.Name.StartsWith("List") && fromValue is not null) + { + fromValue = MapListOfObjects(property, fromValue); + } + + if (propertyFrom.PropertyType.IsEnum && property.PropertyType == typeof(string) && fromValue != null) + { + fromValue = fromValue.ToString(); + } + else if (!propertyFrom.PropertyType.IsEnum && property.PropertyType.IsEnum && fromValue is not null) + { + if (fromValue is string stringValue) + fromValue = Enum.Parse(property.PropertyType, stringValue); + else + fromValue = Enum.ToObject(property.PropertyType, fromValue); + } + + if (fromValue is not null) + { + if (propertyFrom.PropertyType.IsClass + && property.PropertyType.IsClass + && propertyFrom.PropertyType != typeof(string) + && property.PropertyType != typeof(string) + && !property.PropertyType.IsAssignableFrom(propertyFrom.PropertyType)) + { + try + { + var nestedInstance = Activator.CreateInstance(property.PropertyType); + if (nestedInstance != null) + { + var nestedMapped = MapObject(fromValue, nestedInstance); + property.SetValue(newObject, nestedMapped); + continue; + } + } + catch + { + // ignore + } + } + + property.SetValue(newObject, fromValue); + } + } + + // полей + var fieldsTo = typeTo.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + var fieldsFrom = typeFrom.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + foreach (var field in fieldsTo) + { + if (field.Name.Contains("k__BackingField")) + continue; + + if (field.GetCustomAttribute() is not null) + continue; + + var sourceField = fieldsFrom.FirstOrDefault(f => f.Name == field.Name); + object? fromValue = null; + + if (sourceField is not null) + { + fromValue = sourceField.GetValue(obj); + } + else + { + var propertyName = field.Name.TrimStart('_'); + var sourceProperty = typeFrom.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (sourceProperty is not null && sourceProperty.CanRead) + { + fromValue = sourceProperty.GetValue(obj); + } + } + + if (fromValue is null) + continue; + + if (field.FieldType.IsClass && field.FieldType != typeof(string)) + { + try + { + var nested = Activator.CreateInstance(field.FieldType)!; + var mapped = MapObject(fromValue, nested); + RemoveReadOnly(field); + field.SetValue(newObject, mapped); + continue; + } + catch + { + // ignore + } + } + + RemoveReadOnly(field); + field.SetValue(newObject, fromValue); + } + + var classPostProcessing = typeTo.GetCustomAttribute(); + if (classPostProcessing is not null && classPostProcessing.MappingCallMethodName is not null) + { + var methodInfo = typeTo.GetMethod(classPostProcessing.MappingCallMethodName, BindingFlags.NonPublic | BindingFlags.Instance); + methodInfo?.Invoke(newObject, []); + } + + return newObject; + } + + private static void RemoveReadOnly(FieldInfo field) + { + if (!field.IsInitOnly) + return; + + var attr = typeof(FieldInfo).GetField("m_fieldAttributes", BindingFlags.Instance | BindingFlags.NonPublic); + if (attr != null) + { + var current = (FieldAttributes)attr.GetValue(field)!; + attr.SetValue(field, current & ~FieldAttributes.InitOnly); + } + } + + public static To MapObject(object obj) => MapObject(obj, Activator.CreateInstance()!); + + public static To? MapObjectWithNull(object? obj) => obj is null ? default : MapObject(obj, Activator.CreateInstance()); + + private static PropertyInfo? TryGetPropertyFrom(PropertyInfo propertyTo, PropertyInfo[] propertiesFrom) + { + var customAttribute = propertyTo.GetCustomAttributes()? + .ToArray() + .FirstOrDefault(x => propertiesFrom.Any(y => y.Name == x.AlternativeName)); + if (customAttribute is not null) + { + return propertiesFrom.FirstOrDefault(x => x.Name == customAttribute.AlternativeName); + } + return propertiesFrom.FirstOrDefault(x => x.Name == propertyTo.Name); + } + + private static object? PostProcessing(object? value, PostProcessingAttribute postProcessingAttribute, T newObject) + { + if (value is null || newObject is null) + { + return null; + } + if (!string.IsNullOrEmpty(postProcessingAttribute.MappingCallMethodName)) + { + var methodInfo = + newObject.GetType().GetMethod(postProcessingAttribute.MappingCallMethodName, BindingFlags.NonPublic | BindingFlags.Instance); + if (methodInfo is not null) + { + return methodInfo.Invoke(newObject, [value]); + } + } + else if (postProcessingAttribute.ActionType != PostProcessingType.None) + { + switch (postProcessingAttribute.ActionType) + { + case PostProcessingType.ToUniversalTime: + return ToUniversalTime(value); + case PostProcessingType.ToLocalTime: + return ToLocalTime(value); + } + } + return null; + } + + private static object? ToLocalTime(object? obj) + { + if (obj is DateTime date) + return date.ToLocalTime(); + return obj; + } + + private static object? ToUniversalTime(object? obj) + { + if (obj is DateTime date) + return date.ToUniversalTime(); + return obj; + } + + private static void FindAndMapDefaultValue(PropertyInfo property, T newObject) + { + var defaultValueAttribute = property.GetCustomAttribute(); + if (defaultValueAttribute is null) + { + return; + } + if (defaultValueAttribute.DefaultValue is not null) + { + property.SetValue(newObject, defaultValueAttribute.DefaultValue); + return; + } + + var value = defaultValueAttribute.Func switch + { + DefaultValueFunc.UtcNow => DateTime.UtcNow, + _ => (object?)null, + }; + if (value is not null) + { + property.SetValue(newObject, value); + } + } + + private static object? MapListOfObjects(PropertyInfo propertyTo, object list) + { + var listResult = Activator.CreateInstance(propertyTo.PropertyType); + var elementType = propertyTo.PropertyType.GenericTypeArguments[0]; + + foreach (var elem in (IEnumerable)list) + { + object? newElem; + + if (elementType.IsPrimitive || elementType == typeof(string) || elementType == typeof(decimal) || elementType == typeof(DateTime)) + { + newElem = elem; + } + else + { + newElem = MapObject(elem, Activator.CreateInstance(elementType)!); + } + + if (newElem is not null) + { + propertyTo.PropertyType.GetMethod("Add")!.Invoke(listResult, [newElem]); + } + } + + return listResult; + } +} + +enum DefaultValueFunc +{ + None, + UtcNow +} \ No newline at end of file diff --git a/CandyHouseSolution/CandyHouseContracts/Mapper/DefaultValueAttribute.cs b/CandyHouseSolution/CandyHouseContracts/Mapper/DefaultValueAttribute.cs new file mode 100644 index 0000000..34d3448 --- /dev/null +++ b/CandyHouseSolution/CandyHouseContracts/Mapper/DefaultValueAttribute.cs @@ -0,0 +1,12 @@ +namespace MagicCarpetContracts.Mapper; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)] +class DefaultValueAttribute : Attribute +{ + public object? DefaultValue { get; set; } + + public string? FuncName { get; set; } + + public DefaultValueFunc Func { get; set; } = DefaultValueFunc.None; + +} \ No newline at end of file diff --git a/CandyHouseSolution/CandyHouseContracts/Mapper/IgnoreMappingAttribute.cs b/CandyHouseSolution/CandyHouseContracts/Mapper/IgnoreMappingAttribute.cs new file mode 100644 index 0000000..07b0327 --- /dev/null +++ b/CandyHouseSolution/CandyHouseContracts/Mapper/IgnoreMappingAttribute.cs @@ -0,0 +1,6 @@ +namespace MagicCarpetContracts.Mapper; + +[AttributeUsage(AttributeTargets.Property)] +class IgnoreMappingAttribute : Attribute +{ +} \ No newline at end of file diff --git a/CandyHouseSolution/CandyHouseContracts/Mapper/PostProcessingAttribute.cs b/CandyHouseSolution/CandyHouseContracts/Mapper/PostProcessingAttribute.cs new file mode 100644 index 0000000..fe4e3a2 --- /dev/null +++ b/CandyHouseSolution/CandyHouseContracts/Mapper/PostProcessingAttribute.cs @@ -0,0 +1,9 @@ +namespace MagicCarpetContracts.Mapper; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Field)] +class PostProcessingAttribute : Attribute +{ + public string? MappingCallMethodName { get; set; } + + public PostProcessingType ActionType { get; set; } = PostProcessingType.None; +} diff --git a/CandyHouseSolution/CandyHouseContracts/Mapper/PostProcessingType.cs b/CandyHouseSolution/CandyHouseContracts/Mapper/PostProcessingType.cs new file mode 100644 index 0000000..3def2b2 --- /dev/null +++ b/CandyHouseSolution/CandyHouseContracts/Mapper/PostProcessingType.cs @@ -0,0 +1,10 @@ +namespace MagicCarpetContracts.Mapper; + +enum PostProcessingType +{ + None = -1, + + ToUniversalTime = 1, + + ToLocalTime = 2 +}