This content originally appeared on DEV Community and was authored by Ryan English
Using LINQ Expression Trees to build maintainable DTO modeling
One of the biggest problems I faced working on a medium sized .NET Framework code base was writing maintainable modeling systems without excessive code duplication.
It becomes all too easy to utilize extension methods to model your DTOs from a given query's select, or to pull out too much data from your data set in order to instantiate a DTO. With expression tress you can minimize the data pulls as well as create simple mappings of the data.
Let's say we have two classes in our dataset: Student and Class.
public class Student
{
public int Id { get; set; }
public string Name { set; set; }
public virtual ICollection<Class> Classes { get; set; }
}
public class Class
{
public int Id { get; set; }
public string Name { set; set; }
public virtual ICollection<Class> Students { get; set; }
}
And we have two DTOs: StudentModel and ClassModel.
public class StudentModel
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ClassModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<StudentModel> Students { get; set; }
}
There are already two obvious errors you can make here: having code duplication generate the StudentModel depending on the entry point of the code, or having to pull all the properties out of the database (in this case not a big deal) in order to model your DTOs in memory.
/* Utilizing LINQ to Entities you can see
* how we have to specify StudentModels mappings
* in the class models select statement
*/
return database.Classes
.Select(cl => new ClassModel
{
Id = cl.Id
Students = cl.Students.Select(st => new StudentModel
{
Id = st.Id
}
})
.ToList()
// Or we could pull all the data in order to get a cleaner modeling
return database.Classes
.ToList()
.Select(cl => new ClassModel(cl))
.ToList()
Alternatively, you can generate expression trees to handle the modeling, which allows you to focus on specifying the binding between the data and the property. This is exactly how the first Select above operates, the difference is we can build the trees in order to reuse pieces throughout the system.
So let's take that same ClassModel and generate its expression tree like so:
public class ClassModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<StudentModel> Students { get; set; }
/*
* Here we define how a the source of
* the object is bound to this object
* The passed in expression is the parameter of
* select statement, in this case will be the type of Class
*/
public static MemberAssignment[] Assignments(Expression param)
{
return new MemberAssignment[]
{
/*
* Here we are saying we want to bind the property Id on
* ClassModel to the property Id
* on the passed param (Class)
*/
Expression
.Bind(
typeof(ClassModel).GetProperty("Id"),
Expression.Property(param, "Id")
),
// Same for name as above
Expression
.Bind(
typeof(ClassModel).GetProperty("Name"),
Expression.Property(param, "Name")
),
/*
* Here it gets a bit tricker, we are still binding
* students to students but we need to get a list
* (See StudentModel)
*/
Expression
.Bind(
typeof(ClassModel).GetProperty("Students"),
StudenModel.InitList(
Expression.Property(param, "Students")
)
)
};
}
/*
* Here is how the ClassModel is "newed" it creates
* a new ClassModel with a parameterless constructor
* and binds all the known bindings to body, generating
* new ClassModel { // Insert Bindings }
*/
public static Expression<Func<Class, ClassModel>> Init()
{
// Define the soruce of the expressions type
// in this case class dbo
ParameterExpression param =
Expression.Parameter(typeof(Class), "source");
// New the class model using the above bindings
MemberInitExpression init =
Expression.MemberInit(
Expression.New(typeof(ClassModel)),
Assignments(param)
);
// Return the lambda which is
// source => new ClassModel { Id = source.Id, Name = source.Name }
return Expression.Lambda<Func<Class, ClassModel>>(init, param);
}
}
In the above code, we can create reusable expression trees to model ClassModel no matter the data source, by specifying how a Class object binds to a ClassModel object - as long as we have a mapping. This becomes pretty apparent when we look at the StudentModel.
public class StudentModel
{
public int Id { get; set; }
public string Name { get; set; }
// Same as the class model define the assignments
public static MemberAssignment[] Assignments(Expression param)
{
return new MemberAssignment[]
{
/*
* Here we are saying we want to bind the property Id
* on StudentModel to the property Id
* on the passed param (Student)
*/
Expression.Bind(
typeof(StudentModel).GetProperty("Id"),
Expression.Property(param, "Id")
),
// Same for name as above
Expression.Bind(
typeof(StudentModel).GetProperty("Name"),
Expression.Property(param, "Name")
),
};
}
// Just init a single StudentModel
public static Expression<Func<Student, StudentModel>> Init()
{
ParameterExpression param =
Expression.Parameter(typeof(Student), "source");
MemberInitExpression init =
Expression.MemberInit(
Expression.New(typeof(StudentModel)),
Assignments(param)
);
return Expression.Lambda<Func<Student, StudentModel>>(init, param);
}
/*
* This is a bit out of the scope of the article,
* but I wanted to include it to show the capablities
* Basically here we are generating the sub tree
* that will select and to list all the students in the class
*/
public static MethodCallExpression InitList(Expression param)
{
/*
* Just like the member init we have to bind
* the source of the select statement
* to the type of Student
*/
ParameterExpression studentParam =
Expression.Parameter(typeof(Student), "source");
/*
* Next we new a student model with the assigments from above
* So now we have
* new StudentModel{ Id = source.Id, Name = soruce.Name }
*/
MemberInitExpression studentModelInit =
Expression.MemberInit(
Expression.New(typeof(StudentModel)),
Assignments(studentParam)
);
/*
* And finally smack these two together in a
* lambda statement to generate
* source =>
* new StudentModel{ Id = source.Id, Name = soruce.Name }
*/
LambdaExpression lambda =
Expression.Lambda<Func<Student, StudentModel>>
(studentModelInit, studentParam);
/*
* Now we have to specify we are making a call
* to the select statement and specify the types
*/
MethodCallExpression studentSelect = Expression.Call(
null,
GetSelect().MakeGenericMethod(new Type[] {
// Our select "in"
typeof(Student),
// Our select "out"
typeof(StudentModel)
}),
new Expression[] {
// The binding param (a list of students)
param,
// The lamda body from above
lambda
});
// And finally to list it to get all of them
return Expression.Call(
typeof(Enumerable),
"ToList",
// Same here define the type of the out
new Type[] { typeof(StudentModel) },
// The "body" of the to list
studentSelect
);
}
}
Now that we have all the bindings defined, we can make queries as so:
return database.Classes
.Select(ClassModel.Init())
.ToList();
return database.Students
.Select(StudentModel.Init())
.ToList()
You might be thinking, "Wow, that is a lot of extra code and overhead in order to generate models" and I thought the same thing, which is why I created modelLINQ (there are alternatives like AutoMapper).
Using modelLINQ you can write smaller easier-to-digest modeling systems in order to create maintainable code. For example, I swapped the above code using modelLINQ.
public class StudentModel
{
public int Id { get; set; }
public string Name { get; set; }
/*
* All we need to do is setup the init function
* I like to use From<Source> syntax
*/
public static Func<Expression, MemberAssignments[]> FromStudent()
param =>
new MemberAssigment[] {
// Directly bind from source to result on both properties
param.DirectBind<StudentModel>("Id"),
param.DirectBind<StudentModel>("Name")
};
}
public class ClassModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<StudentModel> Students { get; set; }
public static Func<Expression, MemberAssignments[]> FromClass()
param =>
new MemberAssigment[] {
// Directly bind from source to result on both properties
param.DirectBind<StudentModel>("Id"),
param.DirectBind<StudentModel>("Name"),
/*
* Creates a list of StudentModels
* We have to specify the binding model, the source,
* and the result in order to generate the list mapping
*/
param.BindSelectedList<ClassModel, Class, StudentModel>
("Students", "Students", StudentModel.FromStudent)
};
}
// How to query
database.Class
.Select(ClassModel.FromClass.Model<Class, ClassModel>())
.ToList()
Thanks for reading; I hope someone finds it useful! If you have any thoughts, or believe I am wrong, please comment I'd love to hear your feedback!
This content originally appeared on DEV Community and was authored by Ryan English
Ryan English | Sciencx (2021-07-02T22:00:36+00:00) Using LINQ Expression Trees to build maintainable DTO modeling. Retrieved from https://www.scien.cx/2021/07/02/using-linq-expression-trees-to-build-maintainable-dto-modeling/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.