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 Interface Layer UI Model Service Layer Domain Model Data Access Layer Database torsdag 6 mars 14 1 Domain class 1 DAO Interface Layer UI Model Service Layer Domain Model Data Access Layer Database torsdag 6 mars 14 1 Domain class 1 DAO Interface Layer LoginService login(string user) logout(string user) UI Model Service Layer Domain Model Data Access Layer Person firstName lastName employments address PersonDAO getAll() get(int id) getByUserName(string userName) ... SQL query tabulated result sets Database DB torsdag 6 mars 14 1 Domain class 1 DAO Interface Layer LoginService login(string user) logout(string user) UI Model Service Layer Domain Model Data Access Layer Person firstName lastName employments address PersonDAO getAll() get(int id) getByUserName(string userName) ... SQL query tabulated result sets Database GradeDAO Grade grade date Course code credits teacher students torsdag 6 mars 14 getAll() get(int id) getBy(string studentName, string courseName) CourseDAO ... getAll() get(int id) getByName(string courseName) ... DB Related data var people = new PersonDAO().GetAll() Person firstName: "Ola" lastName: "Leifler" employments address Person firstName: "Berit" lastName: "Larsson" employments address 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) torsdag 6 mars 14 null null null null Legacy support Person firstName lastName employments address PersonDAO getAll() get(int id) getByEmployment(int id) getWithAddress(int id) "getAll" DB fName: "Ola" lName: "Leifler" fName: "Berit" lName: "Jansson" PersonDAO getAll() get(int id) getByEmployment(int id) getWithAddress(int id) "getWithAddress" DB firstName: "Ola" llastName: "Leifler" streetaddress: "Storgatan 1" city: "Linköping" torsdag 6 mars 14 Legacy support Person firstName lastName employments address PersonDAO getAll() get(int id) getByEmployment(int id) getWithAddress(int id) "getAll" DB fName: "Ola" lName: "Leifler" fName: "Berit" lName: "Jansson" PersonDAO getAll() get(int id) getByEmployment(int id) getWithAddress(int id) "getWithAddress" DB firstName: "Ola" llastName: "Leifler" streetaddress: "Storgatan 1" city: "Linköping" torsdag 6 mars 14 Legacy support Person firstName lastName employments address PersonDAO getAll() get(int id) getByEmployment(int id) getWithAddress(int id) "getAll" DB fName: "Ola" lName: "Leifler" Address streetaddress city fName: "Berit" lName: "Jansson" PersonDAO getAll() get(int id) getByEmployment(int id) getWithAddress(int id) "getWithAddress" DB firstName: "Ola" llastName: "Leifler" streetaddress: "Storgatan 1" city: "Linköping" 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 var people = GenericDAO<Domain.Person>.Get("GetPeopleByRole", new { RoleId = 3 }); people.Should().NotBeEmpty(); 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; } torsdag 6 mars 14 1 Generic DAO var people = GenericDAO<Domain.Person>.Get("GetPeopleByRole", new { RoleId = 3 }); people.Should().NotBeEmpty(); 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; } torsdag 6 mars 14 1 Generic DAO var people = GenericDAO<Domain.Person>.Get("GetPeopleByRole", new { RoleId = 3 }); people.Should().NotBeEmpty(); 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; } 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 torsdag 6 mars 14 ”GetAll” ”GetPeopleByRole” x 500 Dynamic name resolution GetAll() GetPeopleByRole() x 500 torsdag 6 mars 14 ”GetAll” ”GetPeopleByRole” x 500 Dynamic name resolution using using using using using using System; System.Collections.Generic; System.Linq; System.Dynamic; Configuration; Extensions; GetAll() GetPeopleByRole() x 500 ”GetAll” ”GetPeopleByRole” x 500 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); } [ ... ] } } torsdag 6 mars 14 Dynamic name resolution using using using using using using System; System.Collections.Generic; System.Linq; System.Dynamic; Configuration; Extensions; GetAll() GetPeopleByRole() x 500 ”GetAll” ”GetPeopleByRole” x 500 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); } [ ... ] } } torsdag 6 mars 14 Dynamic name resolution using using using using using using System; System.Collections.Generic; System.Linq; System.Dynamic; Configuration; Extensions; GetAll() GetPeopleByRole() x 500 ”GetAll” ”GetPeopleByRole” x 500 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); } [ ... ] } } dynamic dao = GenericDAO<Domain.Person>.Dispatch(); IEnumerable<Domain.Person> people = dao.GetPeopleByRole(RoleId: 3); people.Should().NotBeEmpty(); torsdag 6 mars 14 Dynamic name resolution using using using using using using System; System.Collections.Generic; System.Linq; System.Dynamic; Configuration; Extensions; GetAll() GetPeopleByRole() x 500 ”GetAll” ”GetPeopleByRole” x 500 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); } [ ... ] } } dynamic dao = GenericDAO<Domain.Person>.Dispatch(); IEnumerable<Domain.Person> people = dao.GetPeopleByRole(RoleId: 3); people.Should().NotBeEmpty(); 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 classes namespace 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 classes namespace 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(); } } } dynamically intercepts method invocations so that properties that are not loaded are fetched from the database Intercept torsdag 6 mars 14 Markup and configuration torsdag 6 mars 14 " " " " public String Name { get; set;} public String Age { get; set;} " " " " " " [SP("emp_GetEmployment")] [ForeignKey("PersonId")] public virtual ICollection<Employment> Employments { get; set;} Markup and configuration torsdag 6 mars 14 " " " " public String Name { get; set;} public String Age { get; set;} " " " " " " [SP("emp_GetEmployment")] [ForeignKey("PersonId")] public virtual ICollection<Employment> Employments { get; set;} Markup and configuration torsdag 6 mars 14 " " " " public String Name { get; set;} public String Age { get; set;} " " " " " " [SP("emp_GetEmployment")] [ForeignKey("PersonId")] public virtual ICollection<Employment> Employments { get; set;} Markup and configuration torsdag 6 mars 14 " " " " public String Name { get; set;} public String Age { get; set;} " " " " " " [SP("emp_GetEmployment")] [ForeignKey("PersonId")] public virtual ICollection<Employment> Employments { get; set;} 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 john@apple.com 15 073-567890 apple@john.com torsdag 6 mars 14 Id StreetAddress City 20 1 Infinite loop Götene Markup and configuration per_GetPersonsByRole() PerId ContactPrivateId PrivateMobile PrivateEmail ContactCompanyId CompanyMobile CompanyEmail 3 14 070-123456 john@apple.com 15 073-567890 apple@john.com Id StreetAddress City 20 1 Infinite loop Götene ContactInfo Id:14 Mobile: "070-123456" Email: "john@apple.com" Person Id: 3 PrivateContact CompanyContact Address ContactInfo Id: 15 Mobile: "073-567890" Email: "apple@john.com" Address Id: 20 StreetAddress: "1 Infinite loop" City: "Götene" torsdag 6 mars 14 Markup and configuration per_GetPersonsByRole() PerId ContactPrivateId PrivateMobile PrivateEmail ContactCompanyId CompanyMobile CompanyEmail 3 14 070-123456 john@apple.com 15 073-567890 apple@john.com Id StreetAddress City 20 1 Infinite loop Götene ContactInfo Id:14 Mobile: "070-123456" Email: "john@apple.com" Person Id: 3 PrivateContact CompanyContact Address ContactInfo Id: 15 Mobile: "073-567890" Email: "apple@john.com" Address Id: 20 StreetAddress: "1 Infinite loop" City: "Götene" torsdag 6 mars 14 Markup and configuration y.Map("ContactCompanyId").To(c => c.Id); torsdag 6 mars 14 Markup and configuration y.Map("ContactCompanyId").To(c => c.Id); public void To<T1>(Expression<Func<T,T1>> propertySelector) { var selectorExpression = (MemberExpression) propertySelector.Body; var prop = (PropertyInfo) selectorExpression.Member; Configurator.CustomFieldsToPropertiesMap[FieldName]=prop; } torsdag 6 mars 14 Markup and configuration y.Map("ContactCompanyId").To(c => c.Id); public void To<T1>(Expression<Func<T,T1>> propertySelector) { var selectorExpression = (MemberExpression) propertySelector.Body; var prop = (PropertyInfo) selectorExpression.Member; Configurator.CustomFieldsToPropertiesMap[FieldName]=prop; } 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 Generation http://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 Return an expression tree that accepts T and returns a boolean 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); Node that refers to the parameter 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); Node that refers to the 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 }); } 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