Reflection in C# - IDATDDD05/lectures/slides/F03... · 2014-03-06 · Database Interface Layer UI...
Transcript of Reflection in C# - IDATDDD05/lectures/slides/F03... · 2014-03-06 · Database Interface Layer UI...
Reflection in C#Case studies in metaprogramming
torsdag 6 mars 14
GenericDAO
torsdag 6 mars 14
ORM for legacy DB
• DB with T-SQL Stored Procedures (SP)
• Apps written in C#, VB .Net and VB6
• Domain classes require mapping classes
• Different SP:s return different sets of data for the same domain objects
• Related data..
torsdag 6 mars 14
1 Domain class - 1 DAO
Service Layer
Data Access Layer
Domain Model
Database
Interface Layer UI Model
torsdag 6 mars 14
1 Domain class - 1 DAO
Service Layer
Data Access Layer
Domain Model
Database
Interface Layer UI Model
torsdag 6 mars 14
1 Domain class - 1 DAO
firstNamelastNameemploymentsaddress
PersongetAll()get(int id)getByUserName(string userName)...
PersonDAO
DB
SQL query
tabulated result sets
login(string user)logout(string user)
LoginService
Service Layer
Data Access Layer
Domain Model
Database
Interface Layer UI Model
torsdag 6 mars 14
1 Domain class - 1 DAO
firstNamelastNameemploymentsaddress
PersongetAll()get(int id)getByUserName(string userName)...
PersonDAO
DB
SQL query
tabulated result sets
login(string user)logout(string user)
LoginService
Service Layer
Data Access Layer
Domain Model
Database
Interface Layer UI Model
gradedate
Grade getAll()get(int id)getBy(string studentName, string courseName)...
GradeDAO
codecreditsteacherstudents
CoursegetAll()get(int id)getByName(string courseName)...
CourseDAO
torsdag 6 mars 14
Related data
people.SelectMany(p => p.Employments)
Do we know if employments have been fetched?
What if we only want to make one DB call? (Select 1+N Problem)
var people = new PersonDAO().GetAll()
firstName: "Ola"lastName: "Leifler"employmentsaddress
Person
nullnull
firstName: "Berit"lastName: "Larsson"employmentsaddress
Person
nullnull
torsdag 6 mars 14
Legacy supportfirstNamelastNameemploymentsaddress
PersongetAll()get(int id)getByEmployment(int id)getWithAddress(int id)
PersonDAO
DB
fName: "Ola"lName: "Leifler"
fName: "Berit"lName: "Jansson"
"getAll"
getAll()get(int id)getByEmployment(int id)getWithAddress(int id)
PersonDAO
DB
"getWithAddress"
firstName: "Ola"llastName: "Leifler"streetaddress: "Storgatan 1"city: "Linköping"
torsdag 6 mars 14
Legacy supportfirstNamelastNameemploymentsaddress
PersongetAll()get(int id)getByEmployment(int id)getWithAddress(int id)
PersonDAO
DB
fName: "Ola"lName: "Leifler"
fName: "Berit"lName: "Jansson"
"getAll"
getAll()get(int id)getByEmployment(int id)getWithAddress(int id)
PersonDAO
DB
"getWithAddress"
firstName: "Ola"llastName: "Leifler"streetaddress: "Storgatan 1"city: "Linköping"
torsdag 6 mars 14
Legacy supportfirstNamelastNameemploymentsaddress
PersongetAll()get(int id)getByEmployment(int id)getWithAddress(int id)
PersonDAO
DB
fName: "Ola"lName: "Leifler"
fName: "Berit"lName: "Jansson"
"getAll"
getAll()get(int id)getByEmployment(int id)getWithAddress(int id)
PersonDAO
DB
"getWithAddress"
firstName: "Ola"llastName: "Leifler"streetaddress: "Storgatan 1"city: "Linköping"
streetaddresscity
Address
torsdag 6 mars 14
Solution
• 1 Generic Data Access Object class
• Dynamic name resolution of Stored Procedures
• Generated proxy classes for lazy loading
• Markup and configuration to configure loading related objects, and mapping between data fields and class properties
torsdag 6 mars 14
1 Generic DAO var people = GenericDAO<Domain.Person>.Get("GetPeopleByRole", new { RoleId = 3 }); people.Should().NotBeEmpty();
torsdag 6 mars 14
1 Generic DAO private static ICollection<T> GetEntities(string procedureName,object parameterObject, int limit) { int recordsRead=0; var config=GetConfig(procedureName); IList<T> entities=new ConcurrentList<T>(); try { using(DbCommand command=BuildCommand(procedureName,parameterObject)) { using(IDataReader reader=command.ExecuteReader()) { var fieldNames=reader.Names(); config.Init(command,fieldNames); var allFieldsUsed=config.GetAllFieldsUsedBy(reader); if((config.Policy& GenericDAO.ExceptionPolicy.AbortOnFieldsUnused)!=0&& !fieldNames.IsSubSetOf(allFieldsUsed)) { // Abort if some SQL result fields are unused throw new FieldsUnusedFromQueryException(fieldNames.Except(allFieldsUsed)); } // Add a mapping from the SP to the properties it can set on the current object while(reader.Read()&&++recordsRead<limit) { entities.Add(config.InjectFrom(reader)); } } } entities=config.Process(entities); } finally { DBAccess.CloseConnection(); } return entities; }
var people = GenericDAO<Domain.Person>.Get("GetPeopleByRole", new { RoleId = 3 }); people.Should().NotBeEmpty();
torsdag 6 mars 14
1 Generic DAO private static ICollection<T> GetEntities(string procedureName,object parameterObject, int limit) { int recordsRead=0; var config=GetConfig(procedureName); IList<T> entities=new ConcurrentList<T>(); try { using(DbCommand command=BuildCommand(procedureName,parameterObject)) { using(IDataReader reader=command.ExecuteReader()) { var fieldNames=reader.Names(); config.Init(command,fieldNames); var allFieldsUsed=config.GetAllFieldsUsedBy(reader); if((config.Policy& GenericDAO.ExceptionPolicy.AbortOnFieldsUnused)!=0&& !fieldNames.IsSubSetOf(allFieldsUsed)) { // Abort if some SQL result fields are unused throw new FieldsUnusedFromQueryException(fieldNames.Except(allFieldsUsed)); } // Add a mapping from the SP to the properties it can set on the current object while(reader.Read()&&++recordsRead<limit) { entities.Add(config.InjectFrom(reader)); } } } entities=config.Process(entities); } finally { DBAccess.CloseConnection(); } return entities; }
var people = GenericDAO<Domain.Person>.Get("GetPeopleByRole", new { RoleId = 3 }); people.Should().NotBeEmpty();
torsdag 6 mars 14
1 Generic DAO private static ICollection<T> GetEntities(string procedureName,object parameterObject, int limit) { int recordsRead=0; var config=GetConfig(procedureName); IList<T> entities=new ConcurrentList<T>(); try { using(DbCommand command=BuildCommand(procedureName,parameterObject)) { using(IDataReader reader=command.ExecuteReader()) { var fieldNames=reader.Names(); config.Init(command,fieldNames); var allFieldsUsed=config.GetAllFieldsUsedBy(reader); if((config.Policy& GenericDAO.ExceptionPolicy.AbortOnFieldsUnused)!=0&& !fieldNames.IsSubSetOf(allFieldsUsed)) { // Abort if some SQL result fields are unused throw new FieldsUnusedFromQueryException(fieldNames.Except(allFieldsUsed)); } // Add a mapping from the SP to the properties it can set on the current object while(reader.Read()&&++recordsRead<limit) { entities.Add(config.InjectFrom(reader)); } } } entities=config.Process(entities); } finally { DBAccess.CloseConnection(); } return entities; }
var people = GenericDAO<Domain.Person>.Get("GetPeopleByRole", new { RoleId = 3 }); people.Should().NotBeEmpty();
torsdag 6 mars 14
Dynamic name resolution
torsdag 6 mars 14
Dynamic name resolution
GetAll()GetPeopleByRole()
x 500
torsdag 6 mars 14
Dynamic name resolution
GetAll()GetPeopleByRole()
x 500
”GetAll””GetPeopleByRole”
x 500
torsdag 6 mars 14
Dynamic name resolution
GetAll()GetPeopleByRole()
x 500
”GetAll””GetPeopleByRole”
x 500
torsdag 6 mars 14
Dynamic name resolution
using System;using System.Collections.Generic;using System.Linq;using System.Dynamic;using Configuration;using Extensions;
namespace Core { public class Dispatcher<T>:DynamicObject where T:class, new() {[ ... ] public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { string procedureName=binder.Name; object paramObj=GetParameterObject(binder,args); string storedProcedureName=GenericDAO<T>.StoredProcedureFullName(procedureName); if(procedureName.StartsWith("Get")) { result=GetResult(paramObj,storedProcedureName); } [ ... ] }}
GetAll()GetPeopleByRole()
x 500
”GetAll””GetPeopleByRole”
x 500
torsdag 6 mars 14
Dynamic name resolution
using System;using System.Collections.Generic;using System.Linq;using System.Dynamic;using Configuration;using Extensions;
namespace Core { public class Dispatcher<T>:DynamicObject where T:class, new() {[ ... ] public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { string procedureName=binder.Name; object paramObj=GetParameterObject(binder,args); string storedProcedureName=GenericDAO<T>.StoredProcedureFullName(procedureName); if(procedureName.StartsWith("Get")) { result=GetResult(paramObj,storedProcedureName); } [ ... ] }}
GetAll()GetPeopleByRole()
x 500
”GetAll””GetPeopleByRole”
x 500
torsdag 6 mars 14
dynamic dao = GenericDAO<Domain.Person>.Dispatch(); IEnumerable<Domain.Person> people = dao.GetPeopleByRole(RoleId: 3); people.Should().NotBeEmpty();
Dynamic name resolution
using System;using System.Collections.Generic;using System.Linq;using System.Dynamic;using Configuration;using Extensions;
namespace Core { public class Dispatcher<T>:DynamicObject where T:class, new() {[ ... ] public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { string procedureName=binder.Name; object paramObj=GetParameterObject(binder,args); string storedProcedureName=GenericDAO<T>.StoredProcedureFullName(procedureName); if(procedureName.StartsWith("Get")) { result=GetResult(paramObj,storedProcedureName); } [ ... ] }}
GetAll()GetPeopleByRole()
x 500
”GetAll””GetPeopleByRole”
x 500
torsdag 6 mars 14
dynamic dao = GenericDAO<Domain.Person>.Dispatch(); IEnumerable<Domain.Person> people = dao.GetPeopleByRole(RoleId: 3); people.Should().NotBeEmpty();
Dynamic name resolution
using System;using System.Collections.Generic;using System.Linq;using System.Dynamic;using Configuration;using Extensions;
namespace Core { public class Dispatcher<T>:DynamicObject where T:class, new() {[ ... ] public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { string procedureName=binder.Name; object paramObj=GetParameterObject(binder,args); string storedProcedureName=GenericDAO<T>.StoredProcedureFullName(procedureName); if(procedureName.StartsWith("Get")) { result=GetResult(paramObj,storedProcedureName); } [ ... ] }}
GetAll()GetPeopleByRole()
x 500
”GetAll””GetPeopleByRole”
x 500
torsdag 6 mars 14
Generated proxy classes protected internal T CreateObject<T>(IDataReader reader,
IEnumerable<string> allowedFields=null) where T:class, new() {
T proxy=ProxyGenerator.CreateClassProxy<T>(ProxygenerationOptions,
new LazyLoadingInterceptor<T>());
foreach(var fieldAndProperty in GetFieldsToPropertiesMap<T>(reader,
allowedFields)) {
var value=GetValue(reader[fieldAndProperty.Key],
fieldAndProperty.Value);
if(value!=null) {
GetSetter(fieldAndProperty.Value)(proxy,
value);
}
}
return proxy;
}
torsdag 6 mars 14
Generated proxy classes protected internal T CreateObject<T>(IDataReader reader,
IEnumerable<string> allowedFields=null) where T:class, new() {
T proxy=ProxyGenerator.CreateClassProxy<T>(ProxygenerationOptions,
new LazyLoadingInterceptor<T>());
foreach(var fieldAndProperty in GetFieldsToPropertiesMap<T>(reader,
allowedFields)) {
var value=GetValue(reader[fieldAndProperty.Key],
fieldAndProperty.Value);
if(value!=null) {
GetSetter(fieldAndProperty.Value)(proxy,
value);
}
}
return proxy;
}
torsdag 6 mars 14
Generated proxy classesnamespace Support {
public class LazyLoadingInterceptor<T>:BaseInterceptor,IInterceptor where T:class, new() {
[ ... ]
public void Intercept(IInvocation invocation) {
object proxy=invocation.Proxy;
// Get the target property access method
var target=invocation.MethodInvocationTarget;
string propertyName=GetPropertyName(target.Name);
if(target.Name.StartsWith("get_")) {
if(!IsPropertyLoaded(propertyName)) {
// Ignore the returned enumeration of elements from Prefetcher<T> as it is just the original sequence with properties set
Prefetcher<T>.FetchRelatedProperty(new List<T>() { proxy as T },typeof(T).GetProperty(propertyName));
SetPropertyLoaded(propertyName);
}
} else {
// Setter invocation: update "property loaded" map with indication of whether non-default value set
var setterValue=invocation.GetArgumentValue(0);
SetPropertyLoaded(propertyName,setterValue!=setterValue.GetType().DefaultValue());
}
invocation.Proceed();
}
}
}
torsdag 6 mars 14
Generated proxy classesnamespace Support {
public class LazyLoadingInterceptor<T>:BaseInterceptor,IInterceptor where T:class, new() {
[ ... ]
public void Intercept(IInvocation invocation) {
object proxy=invocation.Proxy;
// Get the target property access method
var target=invocation.MethodInvocationTarget;
string propertyName=GetPropertyName(target.Name);
if(target.Name.StartsWith("get_")) {
if(!IsPropertyLoaded(propertyName)) {
// Ignore the returned enumeration of elements from Prefetcher<T> as it is just the original sequence with properties set
Prefetcher<T>.FetchRelatedProperty(new List<T>() { proxy as T },typeof(T).GetProperty(propertyName));
SetPropertyLoaded(propertyName);
}
} else {
// Setter invocation: update "property loaded" map with indication of whether non-default value set
var setterValue=invocation.GetArgumentValue(0);
SetPropertyLoaded(propertyName,setterValue!=setterValue.GetType().DefaultValue());
}
invocation.Proceed();
}
}
} Intercept dynamically intercepts method invocations so that properties that are not loaded are fetched
from the databasetorsdag 6 mars 14
Markup and configuration
" " public String Name { get; set;}" " public String Age { get; set;}
" " [SP("emp_GetEmployment")]" " [ForeignKey("PersonId")]" " public virtual ICollection<Employment> Employments { get; set;}
torsdag 6 mars 14
Markup and configuration
" " public String Name { get; set;}" " public String Age { get; set;}
" " [SP("emp_GetEmployment")]" " [ForeignKey("PersonId")]" " public virtual ICollection<Employment> Employments { get; set;}
torsdag 6 mars 14
Markup and configuration
" " public String Name { get; set;}" " public String Age { get; set;}
" " [SP("emp_GetEmployment")]" " [ForeignKey("PersonId")]" " public virtual ICollection<Employment> Employments { get; set;}
torsdag 6 mars 14
Markup and configuration
" " public String Name { get; set;}" " public String Age { get; set;}
" " [SP("emp_GetEmployment")]" " [ForeignKey("PersonId")]" " public virtual ICollection<Employment> Employments { get; set;}
torsdag 6 mars 14
Markup and configuration
GenericDAO<Person>.Configure("per_GetPeopleWithAddress").By(x => { x.ScanForRelatedTypes(GenericDAO.FetchRelatedObjectsPolicy.ScanFields); });
Scan result set for field names that match properties of related objects
torsdag 6 mars 14
Markup and configuration
GenericDAO<Person>.Configure("GetPeopleByRole").By(x => { x.Map("PerId").To(p => p.Id); x.Include<ContactInfo>().By(y => { y.Map("ContactPrivateId").To(c => c.Id); y.Map("PrivateMobile").To(c => c.Mobile); y.Map("PrivateEmail").To(c => c.Email); y.Through(p => p.PrivateContact); }); x.Include<ContactInfo>().By(y => { y.Map("ContactCompanyId").To(c => c.Id); y.Map("CompanyMobile").To(c => c.Mobile); y.Map("CompanyEmail").To(c => c.Email); y.Through(p => p.CompanyContact); }); x.Include<Address>(); });
Map each row in the result set to a main Person object, 2x ContactInfo and an Address
torsdag 6 mars 14
Markup and configuration
per_GetPersonsByRole()
torsdag 6 mars 14
Markup and configuration
per_GetPersonsByRole()
PerId ContactPrivateId PrivateMobile PrivateEmail ContactCompanyId CompanyMobile CompanyEmail
3 14 070-123456 [email protected] 15 073-567890 [email protected]
Id StreetAddress City
20 1 Infinite loop Götene
torsdag 6 mars 14
Markup and configuration
per_GetPersonsByRole()
PerId ContactPrivateId PrivateMobile PrivateEmail ContactCompanyId CompanyMobile CompanyEmail
3 14 070-123456 [email protected] 15 073-567890 [email protected]
Id StreetAddress City
20 1 Infinite loop Götene
Id: 3PrivateContactCompanyContactAddress
Person
Id:14Mobile: "070-123456"Email: "[email protected]"
ContactInfo
Id: 20StreetAddress: "1 Infinite loop"City: "Götene"
Address
Id: 15Mobile: "073-567890"Email: "[email protected]"
ContactInfo
torsdag 6 mars 14
Markup and configuration
per_GetPersonsByRole()
PerId ContactPrivateId PrivateMobile PrivateEmail ContactCompanyId CompanyMobile CompanyEmail
3 14 070-123456 [email protected] 15 073-567890 [email protected]
Id StreetAddress City
20 1 Infinite loop Götene
Id: 3PrivateContactCompanyContactAddress
Person
Id:14Mobile: "070-123456"Email: "[email protected]"
ContactInfo
Id: 20StreetAddress: "1 Infinite loop"City: "Götene"
Address
Id: 15Mobile: "073-567890"Email: "[email protected]"
ContactInfo
torsdag 6 mars 14
Markup and configuration
y.Map("ContactCompanyId").To(c => c.Id);
torsdag 6 mars 14
Markup and configuration
public void To<T1>(Expression<Func<T,T1>> propertySelector) { var selectorExpression = (MemberExpression) propertySelector.Body; var prop = (PropertyInfo) selectorExpression.Member; Configurator.CustomFieldsToPropertiesMap[FieldName]=prop; }
y.Map("ContactCompanyId").To(c => c.Id);
torsdag 6 mars 14
Markup and configuration
public void To<T1>(Expression<Func<T,T1>> propertySelector) { var selectorExpression = (MemberExpression) propertySelector.Body; var prop = (PropertyInfo) selectorExpression.Member; Configurator.CustomFieldsToPropertiesMap[FieldName]=prop; }
y.Map("ContactCompanyId").To(c => c.Id);
FieldName is the name of the field returned in the result set (ContactCompanyId)
torsdag 6 mars 14
https://github.com/olale/GenericDAO
torsdag 6 mars 14
Expression Generationhttp://msdn.microsoft.com/en-us/library/bb882637.aspx
torsdag 6 mars 14
public Expression<Func<T,Boolean>> CreateFilterExpression<T>(params T[] args) { var x = "x"; var paramExp = Expression.Parameter(typeof(T), x); Expression t = Expression.Constant(false); return Expression.Lambda<Func<T,Boolean>>(args.Aggregate(t, " " " " " " " (expr, arg) => " " " " " " " Expression.OrElse(Expression.Equal(paramExp, Expression.Constant(arg)), " " " " " " " " " expr)), " " " " " new ParameterExpression[] { paramExp }); }
torsdag 6 mars 14
public Expression<Func<T,Boolean>> CreateFilterExpression<T>(params T[] args) { var x = "x"; var paramExp = Expression.Parameter(typeof(T), x); Expression t = Expression.Constant(false); return Expression.Lambda<Func<T,Boolean>>(args.Aggregate(t, " " " " " " " (expr, arg) => " " " " " " " Expression.OrElse(Expression.Equal(paramExp, Expression.Constant(arg)), " " " " " " " " " expr)), " " " " " new ParameterExpression[] { paramExp }); }
Return an expression tree that accepts T and returns a boolean
torsdag 6 mars 14
public Expression<Func<T,Boolean>> CreateFilterExpression<T>(params T[] args) { var x = "x"; var paramExp = Expression.Parameter(typeof(T), x); Expression t = Expression.Constant(false); return Expression.Lambda<Func<T,Boolean>>(args.Aggregate(t, " " " " " " " (expr, arg) => " " " " " " " Expression.OrElse(Expression.Equal(paramExp, Expression.Constant(arg)), " " " " " " " " " expr)), " " " " " new ParameterExpression[] { paramExp }); }
Node that refers to the parameter x
torsdag 6 mars 14
public Expression<Func<T,Boolean>> CreateFilterExpression<T>(params T[] args) { var x = "x"; var paramExp = Expression.Parameter(typeof(T), x); Expression t = Expression.Constant(false); return Expression.Lambda<Func<T,Boolean>>(args.Aggregate(t, " " " " " " " (expr, arg) => " " " " " " " Expression.OrElse(Expression.Equal(paramExp, Expression.Constant(arg)), " " " " " " " " " expr)), " " " " " new ParameterExpression[] { paramExp }); }
Node that refers to the constant false
torsdag 6 mars 14
public Expression<Func<T,Boolean>> CreateFilterExpression<T>(params T[] args) { var x = "x"; var paramExp = Expression.Parameter(typeof(T), x); Expression t = Expression.Constant(false); return Expression.Lambda<Func<T,Boolean>>(args.Aggregate(t, " " " " " " " (expr, arg) => " " " " " " " Expression.OrElse(Expression.Equal(paramExp, Expression.Constant(arg)), " " " " " " " " " expr)), " " " " " new ParameterExpression[] { paramExp }); }
x == arg
torsdag 6 mars 14
public Expression<Func<T,Boolean>> CreateFilterExpression<T>(params T[] args) { var x = "x"; var paramExp = Expression.Parameter(typeof(T), x); Expression t = Expression.Constant(false); return Expression.Lambda<Func<T,Boolean>>(args.Aggregate(t, " " " " " " " (expr, arg) => " " " " " " " Expression.OrElse(Expression.Equal(paramExp, Expression.Constant(arg)), " " " " " " " " " expr)), " " " " " new ParameterExpression[] { paramExp }); }
for all values of arg in args
torsdag 6 mars 14
public Expression<Func<T,Boolean>> CreateFilterExpression<T>(params T[] args) { var x = "x"; var paramExp = Expression.Parameter(typeof(T), x); Expression t = Expression.Constant(false); return Expression.Lambda<Func<T,Boolean>>(args.Aggregate(t, " " " " " " " (expr, arg) => " " " " " " " Expression.OrElse(Expression.Equal(paramExp, Expression.Constant(arg)), " " " " " " " " " expr)), " " " " " new ParameterExpression[] { paramExp }); }
combined with ”||” (OrElse)
torsdag 6 mars 14
public Expression<Func<T,Boolean>> CreateFilterExpression<T>(params T[] args) { var x = "x"; var paramExp = Expression.Parameter(typeof(T), x); Expression t = Expression.Constant(false); return Expression.Lambda<Func<T,Boolean>>(args.Aggregate(t, " " " " " " " (expr, arg) => " " " " " " " Expression.OrElse(Expression.Equal(paramExp, Expression.Constant(arg)), " " " " " " " " " expr)), " " " " " new ParameterExpression[] { paramExp }); }
torsdag 6 mars 14
torsdag 6 mars 14
torsdag 6 mars 14