diff --git a/Automapify.Client/BenchmarkTest.cs b/Automapify.Client/BenchmarkTest.cs index 0b96ae4..6261ea1 100644 --- a/Automapify.Client/BenchmarkTest.cs +++ b/Automapify.Client/BenchmarkTest.cs @@ -14,6 +14,7 @@ public void Setup() { Classroom = new Classroom("Jss2") }; + _student.AddTeacher("Adeola", "Ade"); _students = new List { _student }; } diff --git a/Automapify.Client/MappingService.cs b/Automapify.Client/MappingService.cs index 5002c64..d21f0ca 100644 --- a/Automapify.Client/MappingService.cs +++ b/Automapify.Client/MappingService.cs @@ -2,6 +2,7 @@ using Automapify.Client.Models; using Automapify.Client.Models.Dtos; using Automapify.Services; +using Automapify.Services.Extensions; namespace Automapify.Client { @@ -11,11 +12,14 @@ public static MapifyConfiguration StudentConfig() { return new MapifyConfigurationBuilder() .Map(d => d.Name, s => $"{s.FirstName} {s.LastName}") - .Map(d=> d.Classroom, s => s.Classroom.Room) - .Map(d=> d.Room, s => s.Classroom) + .Map(d => d.Classroom, s => s.Classroom.Room) + .Map(d => d.Room, s => s.Classroom) .Map(d => d.Age, s => s.DateOfBirth.ToAge()) - .Map(d=>d.DOB, s => s.DateOfBirth) - .Map(d=>d.IsDeleted, s => false) + .Map(d => d.DOB, s => s.DateOfBirth) + .Map(d => d.IsDeleted, s => false) + .Map(d => d.Teachers.MapTo(s => s.Name), s => s.Teachers.MapFrom(p => $"{p.FirstName} {p.LastName}")) + .Map(d => d.TeacherNames, s => s.Teachers.MapFrom(p => $"{p.FirstName} {p.LastName}")) + .Map(d => d.Classrooms, s => s.Classrooms) .CreateConfig(); } } diff --git a/Automapify.Client/Models/Dtos/Classroom.cs b/Automapify.Client/Models/Dtos/Classroom.cs index 2374ac1..8ecf6a6 100644 --- a/Automapify.Client/Models/Dtos/Classroom.cs +++ b/Automapify.Client/Models/Dtos/Classroom.cs @@ -8,6 +8,10 @@ namespace Automapify.Client.Models.Dtos { public class Classroom { + public Classroom() + { + + } public Classroom(string room) { Room = room; diff --git a/Automapify.Client/Models/Dtos/StudentDto.cs b/Automapify.Client/Models/Dtos/StudentDto.cs index 5b01709..2cfaaf6 100644 --- a/Automapify.Client/Models/Dtos/StudentDto.cs +++ b/Automapify.Client/Models/Dtos/StudentDto.cs @@ -17,6 +17,12 @@ public class StudentDto [MapProperty("Classroom")] public Classroom Room { get; set; } + public List Classrooms { get; set; } + + public List TeacherNames { get; set; } + + public List Teachers { get; set; } + public void DisplayFullName() { Console.WriteLine(Name); diff --git a/Automapify.Client/Models/Dtos/TeacherDto.cs b/Automapify.Client/Models/Dtos/TeacherDto.cs new file mode 100644 index 0000000..211d1ee --- /dev/null +++ b/Automapify.Client/Models/Dtos/TeacherDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Automapify.Client.Models.Dtos +{ + public class TeacherDto + { + public string Name { get; set; } + } +} diff --git a/Automapify.Client/Models/Student.cs b/Automapify.Client/Models/Student.cs index 4437e82..a3f6d9b 100644 --- a/Automapify.Client/Models/Student.cs +++ b/Automapify.Client/Models/Student.cs @@ -9,6 +9,10 @@ namespace Automapify.Client.Models { public class Student { + public Student() + { + + } public Student(int id, string firstName, string lastName, string dateOfBirth, string _class) { Id = id; @@ -16,6 +20,19 @@ public Student(int id, string firstName, string lastName, string dateOfBirth, st LastName = lastName; DateOfBirth = DateTime.Parse(dateOfBirth); Class = _class; + Teachers = new List(); + Classrooms = new List(); + } + + + public void AddTeacher(string firstName, string lastName) + { + Teachers.Add(new Teacher(firstName, lastName).AssignClassroom(Classroom)); + } + + public void AddClassroom(string room) + { + Classrooms.Add(new Classroom(room)); } public int Id { get; set; } public string FirstName { get; set; } @@ -23,5 +40,7 @@ public Student(int id, string firstName, string lastName, string dateOfBirth, st public DateTime DateOfBirth { get; set; } public string Class { get; set; } public Classroom Classroom { get; set; } + public List Classrooms { get; set; } + public List Teachers { get; set; } } } diff --git a/Automapify.Client/Models/Teacher.cs b/Automapify.Client/Models/Teacher.cs new file mode 100644 index 0000000..f289f1f --- /dev/null +++ b/Automapify.Client/Models/Teacher.cs @@ -0,0 +1,27 @@ +using Automapify.Client.Models.Dtos; + +namespace Automapify.Client.Models +{ + public class Teacher + { + public Teacher() + { + + } + public Teacher(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } + + public Teacher AssignClassroom(Classroom classroom) + { + Classroom = classroom; + return this; + } + + public string FirstName { get; set; } + public string LastName { get; set; } + public Classroom Classroom { get; set; } + } +} \ No newline at end of file diff --git a/Automapify.Client/Program.cs b/Automapify.Client/Program.cs index 8242546..6dff458 100644 --- a/Automapify.Client/Program.cs +++ b/Automapify.Client/Program.cs @@ -8,7 +8,6 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.InProcess.Emit; -using BenchmarkDotNet.Toolchains.InProcess.NoEmit; // Set up student and classroom data var student = new Student(1, "Adeola", "Aderibigbe", "11/12/2000", "jss1"); @@ -16,10 +15,14 @@ var classroom = new Classroom("Jss2"); student.Classroom = classroom; +student.AddClassroom("Jss3"); + +student.AddTeacher("Violet", "James"); // Map a list of students to a list of student dtos using mapping configuration var students = new List() { student }; + var studentDtos = student.Map>(MappingService.StudentConfig()); @@ -65,12 +68,12 @@ item.DisplayAge(); } - +/* var config = DefaultConfig.Instance .AddJob(Job .MediumRun .WithLaunchCount(1) .WithToolchain(InProcessEmitToolchain.Instance)); -//var summary = BenchmarkRunner.Run(typeof(BenchmarkTest),config); +var summary = BenchmarkRunner.Run(typeof(BenchmarkTest), config);*/ diff --git a/Automapify.Test/Models/Classroom.cs b/Automapify.Test/Models/Classroom.cs index 1262ad6..cd8801a 100644 --- a/Automapify.Test/Models/Classroom.cs +++ b/Automapify.Test/Models/Classroom.cs @@ -8,6 +8,10 @@ namespace Automapify.Test.Models { public class Classroom { + public Classroom() + { + + } public Classroom(string name,int noOfStudents, int noOfTeachers,string classCode) { Id = 1; diff --git a/Automapify.Test/Models/Course.cs b/Automapify.Test/Models/Course.cs index fd0945b..9f4dfa0 100644 --- a/Automapify.Test/Models/Course.cs +++ b/Automapify.Test/Models/Course.cs @@ -2,6 +2,10 @@ { public class Course { + public Course() + { + + } public Course(string name, string courseCode) { Id = 1; diff --git a/Automapify.Test/Models/Lecturer.cs b/Automapify.Test/Models/Lecturer.cs index 69299fa..461084e 100644 --- a/Automapify.Test/Models/Lecturer.cs +++ b/Automapify.Test/Models/Lecturer.cs @@ -2,6 +2,10 @@ { public class Lecturer { + public Lecturer() + { + + } public Lecturer(string name) { Id = 1; diff --git a/Automapify.Test/TestHelper.cs b/Automapify.Test/TestHelper.cs index 9f34e4f..f53a5a7 100644 --- a/Automapify.Test/TestHelper.cs +++ b/Automapify.Test/TestHelper.cs @@ -1,4 +1,5 @@ using Automapify.Services; +using Automapify.Services.Extensions; using Automapify.Test.Models; namespace Automapify.Test @@ -34,9 +35,9 @@ private MapifyConfiguration SetupConfiguration() .Map(d=>d.Name,s=>s.Name) .Map(d=>d.NoOfLecturers, s=>s.NumberOfTeachers) .Map(d=>d.NoOfStudents, s=>s.NumberOfStudents) - .Map(d=>d.LeadLecturers, s=>s.Courses.Select(s=>s.LeadLecturer.Name).ToList()) - .Map(d=>d.IsActive, s=>s.NumberOfTeachers > 0 && s.NumberOfStudents > 0) - .Map(d=> d.Code, s=>s.ClassCode.ToString()) + .Map(d=>d.LeadLecturers, s => s.Courses.MapFrom(s=>s.LeadLecturer.Name)) + .Map(d=>d.IsActive, s => s.NumberOfTeachers > 0 && s.NumberOfStudents > 0) + .Map(d=> d.Code, s => s.ClassCode.ToString()) .CreateConfig(); return MapifyConfiguration; } diff --git a/Automapify/Models/MapifyTuple.cs b/Automapify/Models/MapifyTuple.cs index eb50607..5e70c2a 100644 --- a/Automapify/Models/MapifyTuple.cs +++ b/Automapify/Models/MapifyTuple.cs @@ -4,12 +4,23 @@ namespace Automapify.Models { public class MapifyTuple { - public MapifyTuple(string destinationMemberName, LambdaExpression expression) + public MapifyTuple(List destinationMembers, string sourceName ,LambdaExpression sourceExpression) { - DestinationMemberName = destinationMemberName; - SourceExpression = expression.Compile(); + DestinationMemberNames = destinationMembers; + SourceMemberName = sourceName; + SourceExpression = sourceExpression.Compile(); } - public string DestinationMemberName { get; set; } + public MapifyTuple(string destinationMember, string sourceName, LambdaExpression sourceExpression) + { + DestinationMemberNames.Add(destinationMember); + SourceMemberName = sourceName; + SourceExpression = sourceExpression.Compile(); + } + + + public List DestinationMemberNames { get; set; } = new List(); + + public string SourceMemberName { get; set; } public Delegate SourceExpression { get; set; } } } diff --git a/Automapify/Models/Property.cs b/Automapify/Models/Property.cs new file mode 100644 index 0000000..2b2031a --- /dev/null +++ b/Automapify/Models/Property.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Automapify.Models +{ + public class Property + { + public Property(string propertyName) + { + PropertyName = propertyName; + } + + public string SourceName { get; set; } + public string PropertyName { get; set; } + } +} diff --git a/Automapify/Services/Extensions/MappingExtension.cs b/Automapify/Services/Extensions/MappingExtension.cs new file mode 100644 index 0000000..cfa1dba --- /dev/null +++ b/Automapify/Services/Extensions/MappingExtension.cs @@ -0,0 +1,18 @@ +using System.Linq.Expressions; + +namespace Automapify.Services.Extensions +{ + public static class MappingExtension + { + public static Expression> MapFrom(this IEnumerable source, Expression> sourcePredicate) + { + return sourcePredicate; + } + + public static Expression> MapTo(this IEnumerable destination, Expression> sourcePredicate) + { + return sourcePredicate; + } + + } +} diff --git a/Automapify/Services/Extensions/StringExtensions.cs b/Automapify/Services/Extensions/StringExtensions.cs index 40044b9..8a1de88 100644 --- a/Automapify/Services/Extensions/StringExtensions.cs +++ b/Automapify/Services/Extensions/StringExtensions.cs @@ -17,6 +17,14 @@ public static bool NotContains(this string source, string parameter) return !source.Contains(parameter); } + public static bool Contains(this IEnumerable source, string firstParameter, string secondParameter) + { + if (source.Count() == 0) + return true; + + return source.Contains(firstParameter) && source.Contains(secondParameter); + } + public static bool NotStartsWith(this string source, string parameter) { if (string.IsNullOrEmpty(source)) diff --git a/Automapify/Services/MapifyConfigurationBuilder.cs b/Automapify/Services/MapifyConfigurationBuilder.cs index ebdcb23..0a78a74 100644 --- a/Automapify/Services/MapifyConfigurationBuilder.cs +++ b/Automapify/Services/MapifyConfigurationBuilder.cs @@ -16,30 +16,118 @@ public MapifyConfigurationBuilder() /// Destination expression /// Source expression /// - public MapifyConfigurationBuilder Map(Expression> destinationPredicate, Expression> sourcePredicate) + public MapifyConfigurationBuilder Map + (Expression> destinationPredicate, Expression> sourcePredicate) { - mapifyTuples.Add(new MapifyTuple(GetMemberExpressionName(destinationPredicate), sourcePredicate)); + CreateTuple(destinationPredicate, sourcePredicate); return this; } + /// + /// Get expression names from member + /// + /// Type of object to convert to or from + /// Data type + /// Expression + /// Names of the member + private void CreateTuple + (Expression> destinationPredicate, Expression> sourcePredicate) + { + MemberExpression memberExpression = destinationPredicate.Body as MemberExpression; + + var currentMemberExpression = memberExpression ?? (destinationPredicate.Body is UnaryExpression unary ? unary.Operand as MemberExpression : null); + + LambdaExpression newSourceExpression = null; + + var sourceName = GetSourceMemberExpressionName(sourcePredicate); + + if (currentMemberExpression != null) + { + if (sourcePredicate.Body is MethodCallExpression sourceExpression) + { + newSourceExpression = sourcePredicate.Compile().Invoke(Activator.CreateInstance()) as LambdaExpression; + } + mapifyTuples.Add(new MapifyTuple(currentMemberExpression?.Member?.Name, sourceName, newSourceExpression ?? sourcePredicate)); + } + else + { + var destinationName = GetMemberExpressionNames(destinationPredicate); + + newSourceExpression = sourcePredicate.Compile().Invoke(Activator.CreateInstance()) as LambdaExpression; + + mapifyTuples.Add(new MapifyTuple(destinationName, sourceName, newSourceExpression)); + } + } + /// - /// Get expression name from member + /// Get expression names from member /// /// Type of object to convert to or from /// Data type /// Expression - /// Name of the member - private string GetMemberExpressionName(Expression> exp) + /// Names of the member + private string GetSourceMemberExpressionName(Expression> exp) { - MemberExpression member = exp.Body as MemberExpression; - var currentMember = member ?? (exp.Body is UnaryExpression unary ? unary.Operand as MemberExpression : null); - if (currentMember != null) - return currentMember.Member?.Name; + if (exp.Body is not MethodCallExpression methodExpression) + { + if (exp.Body is MemberExpression e) + { + return e.Member?.Name; + } + } + else + { + var expression = methodExpression.Arguments.FirstOrDefault(); + var memberExpression = expression as MemberExpression; + var currentMemberExpression = memberExpression ?? (expression is UnaryExpression un ? (un.Operand is LambdaExpression body ? body.Body as MemberExpression : null) : null); + + if (currentMemberExpression != null) + { + return currentMemberExpression?.Member?.Name; + } + } return default; } + + /// + /// Get expression names from member + /// + /// Type of object to convert to or from + /// Data type + /// Expression + /// Names of the member + private List GetMemberExpressionNames(Expression> exp) + { + var members = new List(); + + if (exp.Body is not MethodCallExpression methodExpression) + { + if (exp.Body is MemberExpression e) + { + members.Add(e.Member?.Name); + } + } + else + { + var expressions = methodExpression.Arguments.Reverse().ToList(); + foreach (var expression in expressions) + { + var memberExpression = expression as MemberExpression; + var currentMemberExpression = memberExpression ?? (expression is UnaryExpression un ? (un.Operand is LambdaExpression body ? body.Body as MemberExpression : null) : null); + + if (currentMemberExpression != null) + { + members.Add(currentMemberExpression?.Member?.Name); + } + } + } + + return members; + } + public MapifyConfiguration CreateConfig() { return new MapifyConfiguration(mapifyTuples); diff --git a/Automapify/Services/Mapper.cs b/Automapify/Services/Mapper.cs index 2c37897..94b6d8d 100644 --- a/Automapify/Services/Mapper.cs +++ b/Automapify/Services/Mapper.cs @@ -1,6 +1,7 @@ using Automapify.Models; using Automapify.Services; using Automapify.Services.Attributes; +using Automapify.Services.Extensions; using Automapify.Services.Utilities; using System.Collections; using System.Reflection; @@ -26,6 +27,8 @@ public static void Map(this TDestination destinationObj, T { var destinationType = destinationObj.GetType(); + var sourceType = typeof(TSource); + var isEnumerable = IsEnumerable(destinationType); if (isEnumerable) @@ -34,7 +37,7 @@ public static void Map(this TDestination destinationObj, T var destination = destinationObj as IList; - MapCollection(sourceObj, destination, destinationType, mapifyConfiguration); + MapCollection(sourceObj, destination, destinationType, sourceType,mapifyConfiguration); destinationObj = (TDestination)destination; @@ -43,7 +46,7 @@ public static void Map(this TDestination destinationObj, T var destinationProperties = destinationType.GetProperties(); - var sourceProperties = sourceObj.GetType().GetProperties(); + var sourceProperties = sourceType.GetProperties(); var mappingAttributes = destinationType.GetAttributes(); @@ -71,6 +74,8 @@ public static TDestination Map(this TSource sourceObj, Ma { var destinationType = typeof(TDestination); + var sourceType = typeof(TSource); + var isEnumerable = IsEnumerable(destinationType); if (isEnumerable) @@ -83,7 +88,7 @@ public static TDestination Map(this TSource sourceObj, Ma var destination = (TDestination)Activator.CreateInstance(target) as IList; - MapCollection(sourceObj, destination, destinationType, mapifyConfiguration); + MapCollection(sourceObj, destination, destinationType,sourceType ,mapifyConfiguration); return (TDestination)destination; } @@ -92,7 +97,7 @@ public static TDestination Map(this TSource sourceObj, Ma var destinationProperties = destinationType.GetProperties(); - var sourceProperties = sourceObj.GetType().GetProperties(); + var sourceProperties = sourceType.GetProperties(); var mappingAttributes = destinationType.GetAttributes(); @@ -108,10 +113,14 @@ public static TDestination Map(this TSource sourceObj, Ma } } - private static void MapCollection(TSource sourceObj, IList destinationObj ,Type destinationType, MapifyConfiguration mapifyConfiguration = null) + private static void MapCollection(TSource sourceObj, IList destinationObj ,Type destinationType, + Type sourceType, + MapifyConfiguration mapifyConfiguration = null) { - var sourceType = typeof(TSource); - + if(sourceObj == null) + { + return; + } var destinationProperties = destinationType.GetProperties(); var mappingAttributes = destinationType.GetAttributes(); @@ -147,6 +156,21 @@ private static void MapCollection(TSource sourceObj, IList } } + + private static void MapCollection(object? sourceObj, IList destination,string destinationName, + MapifyConfiguration mapifyConfiguration = null) + { + var propertyExpression = GetSourceExpression(mapifyConfiguration?.MapifyTuples, destinationName); + if( sourceObj is ICollection sourceCollection) + { + foreach(var source in sourceCollection) + { + var result = propertyExpression?.DynamicInvoke(source); + destination.Add(result); + } + } + } + private static void MapProperties(PropertyInfo[] destinationProperties, PropertyInfo[] sourceProperties,MapifyConfiguration mapifyConfiguration, TSource sourceObj, TDestination destinationObj,Dictionary mappingAttributes) @@ -154,18 +178,45 @@ private static void MapProperties(PropertyInfo[] destinati foreach (var destinationProperty in destinationProperties) { object propertyValue = null; - var propertyExpression = GetSourceExpression(mapifyConfiguration?.MapifyTuples, destinationProperty.Name); - if (propertyExpression != null) + + if (destinationProperty.PropertyType.GetGenericArguments().FirstOrDefault() is Type type && mapifyConfiguration != null) + { + Type openType = typeof(List<>); + + Type target = openType.MakeGenericType(new[] { type }); + + var destination = Activator.CreateInstance(target) as IList; + + var config = GetSourceConfiguration(mapifyConfiguration?.MapifyTuples, destinationProperty.Name, sourceProperties); + + var sourceProperty = sourceProperties.Where(s => config.MapifyTuples.Any(x => x.SourceMemberName == s.Name)).FirstOrDefault(); + + var sourcePropertyValue = sourceProperty.GetValue(sourceObj); + + if (type.Namespace.StartsWith("System")) + { + MapCollection(sourcePropertyValue, destination, destinationProperty.Name, config); + } + else + { + MapCollection(sourcePropertyValue, destination, type, sourcePropertyValue?.GetType(), config); + } + + propertyValue = destination; + } + + else if (GetProperyInfo(mappingAttributes, sourceProperties, destinationProperty) is PropertyInfo propertyInfo) { - propertyValue = propertyExpression.DynamicInvoke(sourceObj); + propertyValue = propertyInfo.GetValue(sourceObj); + } + else if (GetSourceExpression(mapifyConfiguration?.MapifyTuples, destinationProperty.Name) is Delegate propertyExpression) + { + propertyValue = propertyExpression?.DynamicInvoke(sourceObj); + } else { - var sourceProperty = GetProperyInfo(mappingAttributes, sourceProperties, destinationProperty); - if (sourceProperty != null) - { - propertyValue = sourceProperty.GetValue(sourceObj); - } + continue; } if (propertyValue != null) @@ -180,36 +231,49 @@ private static PropertyInfo GetProperyInfo(Dictionary s.SourceType == typeof(TSource)).FirstOrDefault(); - if (currentAttribute == null) - { - var attribute = attributes.FirstOrDefault(); - sourceProperty = sourceProperties.Where(s => s.Name == attribute.PropertyName).FirstOrDefault(); - } - else - { - sourceProperty = sourceProperties.Where(s => s.Name == currentAttribute.PropertyName).FirstOrDefault(); - } + return sourceProperties.Where(s => s.Name == destinationProperty.Name && s.PropertyType == destinationProperty.PropertyType).FirstOrDefault(); + } + + var currentAttribute = attributes.Where(s => s.SourceType == destinationProperty.PropertyType).FirstOrDefault(); + if (currentAttribute != null) + { + sourceProperty = sourceProperties.Where(s => s.Name == currentAttribute.PropertyName).FirstOrDefault(); } else { - sourceProperty = sourceProperties.Where(s => s.Name == destinationProperty.Name).FirstOrDefault(); + currentAttribute = attributes.FirstOrDefault(); + sourceProperty = sourceProperties.Where(s => s.Name == currentAttribute.PropertyName).FirstOrDefault(); } + return sourceProperty; } private static Delegate GetSourceExpression(IList mapifyTuples, string propertyName) + { + if (mapifyTuples == null) + return default; + + MapifyTuple mapifyTuple = mapifyTuples.Where(s => s.DestinationMemberNames.Contains(propertyName)).FirstOrDefault(); + if(mapifyTuple != null) + { + return mapifyTuple.SourceExpression; + } + + return default; + } + + private static MapifyConfiguration GetSourceConfiguration(IList mapifyTuples,string sourcePropertyName, PropertyInfo[] properties) { if (mapifyTuples == null) return default; - var memberExpr = mapifyTuples.Where(s => s.DestinationMemberName == propertyName).FirstOrDefault(); - if(memberExpr != null) + var newMapifyTuples = mapifyTuples.Where(s => s.DestinationMemberNames.Contains(sourcePropertyName) && properties.Any(p => s.SourceMemberName.Contains(p.Name))).ToList(); + if (newMapifyTuples.Count > 0) { - return memberExpr.SourceExpression; + return new MapifyConfiguration(newMapifyTuples); } return default;