/*
Kooboo is a content management system based on ASP.NET MVC framework. Copyright 2009 Yardi Technology Limited.
This program is free software: you can redistribute it and/or modify it under the terms of the
GNU General Public License version 3 as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program.
If not, see http://www.kooboo.com/gpl3/.
*/
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Diagnostics;
using System.Data.Common;
using System.Globalization;
using System;
using System.Data;
using System.Data.Metadata.Edm;
namespace Microsoft.Data.Extensions{
/// <summary>
/// Supports shaping DbCommand and DbDataReader as CLR instances.
/// </summary>
/// <remarks>
/// This type is thread-safe. For performance reasons, static instances of this type
/// should be shared wherever possible. Note that a single instance of the Materializer
/// cannot be used with two command or readers returning different fields or the same
/// fields in a different order. To develop custom optimization behaviors, implement
/// methods with the IMaterializerOptimizedMethodAttribute.
/// </remarks>
/// <typeparam name="T">CLR type to materialize.</typeparam>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Materializer")]
public sealed class Materializer<T> {
private static readonly ParameterExpression s_recordParameter = Expression.Parameter(typeof(IDataRecord), "record");
private static readonly MethodInfo s_fieldOfTOrdinalMethod = typeof(DataExtensions).GetMethod("Field", new Type[] { typeof(IDataRecord), typeof(int) });
private static readonly MethodInfo s_fieldOfTColumnNameMethod = typeof(DataExtensions).GetMethod("Field", new Type[] { typeof(IDataRecord), typeof(string) });
private readonly Expression<Func<IDataRecord, T>> userSuppliedShaper;
private readonly object syncLock = new object();
private Func<IDataRecord, T> shaperDelegate;
private ReadOnlyCollection<string> fieldNames;
/// <summary>
/// Default constructor. Instances of T are materialized by assigning field values to
/// writable properties on T having the same name. By default, allows fields
/// that do not have corresponding properties and properties that do not have corresponding
/// fields.
/// </summary>
public Materializer() {
}
/// <summary>
/// Creates a materializer for the given EDM type. Assumes that a column exists in the result
/// set for every property of the EDM type.
/// </summary>
/// <remarks>
/// Beyond requiring that all properties of the type are available, no type validation
/// is performed.
/// </remarks>
/// <param name="structuralType">EDM type for which to create a Materializer.</param>
public Materializer(StructuralType structuralType)
: this(GetStructuralTypeShaper(structuralType)) {
}
/// <summary>
/// Instances of T are materialized using the given shaper. For every row in the result,
/// the shaper is evaluated.
/// </summary>
/// <param name="shaper">Describes how reader rows are transformed into typed results.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
public Materializer(Expression<Func<IDataRecord, T>> shaper) {
this.userSuppliedShaper = shaper;
}
/// <summary>
/// Materializes the results of the given command.
/// </summary>
/// <param name="command">Command to execute.</param>
/// <returns>Shaped results.</returns>
public IEnumerable<T> Materialize(DbCommand command) {
return Materialize(command, CommandBehavior.Default);
}
/// <summary>
/// Materializes the results of the given command.
/// </summary>
/// <param name="command">Command to execute.</param>
/// <param name="commandBehavior">Command behavior to use when executing the command.</param>
/// <returns>Shaped results.</returns>
public IEnumerable<T> Materialize(DbCommand command, CommandBehavior commandBehavior) {
Utility.CheckArgumentNotNull(command, "command");
using (command.Connection.CreateConnectionScope()) {
using (DbDataReader reader = command.ExecuteReader(commandBehavior)) {
foreach (T element in this.Materialize(reader)) {
yield return element;
}
}
}
}
/// <summary>
/// Materializes rows in the given reader.
/// </summary>
/// <param name="reader">Results to materialize.</param>
/// <returns>Shaped results.</returns>
public IEnumerable<T> Materialize(DbDataReader reader) {
Utility.CheckArgumentNotNull(reader, "reader");
bool first = true;
while (reader.Read()) {
if (first) {
InitializeShaper(reader);
first = false;
}
yield return this.shaperDelegate(reader);
}
yield break;
}
private void InitializeShaper(IDataRecord record) {
// Determine the layout of the record.
if (null != this.fieldNames) {
// If a record layout has already been established, make sure the current
// record is compatible with it.
ValidateFieldNames(record);
}
else {
// Initialize a new shaper delegate within a lock (first one wins).
lock (this.syncLock) {
if (null != this.fieldNames) {
// another thread beat us to it...
ValidateFieldNames(record);
}
else {
// if the user didn't provide an explicit shaper, generate a default shaper
// based on the element type and the record layout.
ReadOnlyCollection<string> recordFieldNames = GetFieldNames(record);
Expression<Func<IDataRecord, T>> shaper = this.userSuppliedShaper ?? GetDefaultShaper(recordFieldNames);
// optimize the expression
Func<IDataRecord, T> compiledShaper = OptimizingExpressionVisitor
.Optimize(recordFieldNames, shaper)
.Compile();
// lock down the Materializer instance to use the (first encountered) field information and delegate
this.fieldNames = recordFieldNames;
this.shaperDelegate = compiledShaper;
}
}
}
}
private static ReadOnlyCollection<string> GetFieldNames(IDataRecord record) {
List<string> fieldNames = new List<string>(record.FieldCount);
fieldNames.AddRange(Enumerable.Range(0, record.FieldCount)
.Select(i => record.GetName(i)));
return fieldNames.AsReadOnly();
}
private void ValidateFieldNames(IDataRecord record) {
if (this.fieldNames.Count != record.FieldCount ||
this.fieldNames.Where((fieldName, ordinal) => record.GetName(ordinal) != fieldName)
.Any()) {
throw new InvalidOperationException(Messages.IncompatibleReader);
}
}
private static Expression<Func<IDataRecord, T>> GetDefaultShaper(ReadOnlyCollection<string> fieldNames) {
ConstructorInfo defaultConstructor = GetDefaultConstructor();
// figure out which fields/properties have corresponding columns
List<MemberBinding> memberBindings = new List<MemberBinding>();
int ordinal = 0;
foreach (string columnName in fieldNames) {
MemberBinding memberBinding;
if (TryCreateMemberBinding(columnName, ordinal, out memberBinding)) {
memberBindings.Add(memberBinding);
}
ordinal++;
}
// record => new T { ColumnName = record.Field<TColumn>(columnOrdinal), ... }
return Expression.Lambda<Func<IDataRecord, T>>(
Expression.MemberInit(
Expression.New(defaultConstructor),
memberBindings),
s_recordParameter);
}
private static Expression<Func<IDataRecord, T>> GetStructuralTypeShaper(StructuralType structuralType) {
ConstructorInfo defaultConstructor = GetDefaultConstructor();
// we expect to find a column for every 'structural' member of the type
List<MemberBinding> memberBindings = new List<MemberBinding>();
foreach (EdmProperty property in structuralType.Members.OfType<EdmProperty>()) {
MemberBinding memberBinding;
if (TryCreateMemberBinding(property.Name, null, out memberBinding)) {
memberBindings.Add(memberBinding);
}
}
// record => new T { ColumnName = record.Field<TColumn>("propertyName1"), ... }
return Expression.Lambda<Func<IDataRecord, T>>(
Expression.MemberInit(
Expression.New(defaultConstructor),
memberBindings),
s_recordParameter);
}
private static ConstructorInfo GetDefaultConstructor() {
ConstructorInfo defaultConstructor = typeof(T).GetConstructor(Type.EmptyTypes);
if (null == defaultConstructor) {
throw new InvalidOperationException(Messages.UnableToCreateDefaultMaterializeDelegate);
}
return defaultConstructor;
}
private static bool TryCreateMemberBinding(string columnName, int? ordinal, out MemberBinding memberBinding) {
PropertyInfo propertyInfo = typeof(T).GetProperty(columnName);
if (null != propertyInfo) {
if (propertyInfo.GetIndexParameters().Length == 0 && propertyInfo.CanWrite) {
memberBinding = Expression.Bind(propertyInfo.GetSetMethod(), CreateGetValueCall(propertyInfo.PropertyType, columnName, ordinal));
return true;
}
}
memberBinding = null;
return false;
}
private static Expression CreateGetValueCall(Type type, string columnName, int? ordinal) {
MethodInfo fieldOfTMethod;
Expression fieldArgument;
if (ordinal.HasValue) {
fieldOfTMethod = s_fieldOfTOrdinalMethod.MakeGenericMethod(type);
fieldArgument = Expression.Constant(ordinal.Value);
}
else {
fieldOfTMethod = s_fieldOfTColumnNameMethod.MakeGenericMethod(type);
fieldArgument = Expression.Constant(columnName);
}
return Expression.Call(fieldOfTMethod, s_recordParameter, fieldArgument);
}
/// <summary>
/// LINQ expression visitor that optimizes method call expressions referencing methods with
/// the IMaterializerMethodAttribute.
/// </summary>
private class OptimizingExpressionVisitor : ExpressionVisitor {
private readonly ReadOnlyCollection<string> fieldNames;
private readonly ParameterExpression recordParameter;
private OptimizingExpressionVisitor(ReadOnlyCollection<string> fieldNames, ParameterExpression recordParameter) {
this.fieldNames = fieldNames;
this.recordParameter = recordParameter;
}
internal static Expression<Func<IDataRecord, T>> Optimize(ReadOnlyCollection<string> fieldNames, Expression<Func<IDataRecord, T>> shaper) {
Utility.CheckArgumentNotNull(fieldNames, "fieldNames");
Utility.CheckArgumentNotNull(shaper, "shaper");
OptimizingExpressionVisitor visitor = new OptimizingExpressionVisitor(fieldNames, shaper.Parameters.Single());
return (Expression<Func<IDataRecord, T>>)visitor.Visit(shaper);
}
protected override Expression VisitMethodCall(MethodCallExpression m) {
Expression result = base.VisitMethodCall(m);
if (result.NodeType == ExpressionType.Call) {
m = (MethodCallExpression)result;
MaterializerOptimizedMethodAttribute attribute = m.Method.GetCustomAttributes(typeof(MaterializerOptimizedMethodAttribute), false)
.Cast<MaterializerOptimizedMethodAttribute>()
.SingleOrDefault(); // multiple attributes not permitted; not inherited
if (null != attribute) {
return attribute.Optimizer.OptimizeMethodCall(this.fieldNames, this.recordParameter, m);
}
}
return result;
}
}
}
/// <summary>
/// Attach this attribute to a method that can be locally rewritten to optimize Materializer
/// performance.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Materializer")]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class MaterializerOptimizedMethodAttribute : Attribute {
private readonly IMaterializerMethodOptimizer optimizer;
/// <summary>
/// Construct attribute.
/// </summary>
/// <param name="optimizerType">A type implementing the IMaterializerMethodOptimizer interface
/// that can be used to optimize MethodCallExpressions referencing the attributed method. The
/// type must have a public default constructor.</param>
public MaterializerOptimizedMethodAttribute(Type optimizerType) {
Utility.CheckArgumentNotNull(optimizerType, "optimizerType");
ConstructorInfo defaultConstructor = optimizerType.GetConstructor(Type.EmptyTypes);
if (!typeof(IMaterializerMethodOptimizer).IsAssignableFrom(optimizerType) ||
null == defaultConstructor) {
throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Messages.InvalidOptimizerType, typeof(IMaterializerMethodOptimizer)), "optimizerType");
}
this.optimizer = (IMaterializerMethodOptimizer)defaultConstructor.Invoke(null);
}
/// <summary>
/// Gets type of optimizer.
/// </summary>
public Type OptimizerType {
get { return this.optimizer.GetType(); }
}
/// <summary>
/// Instance of the optimizer class.
/// </summary>
internal IMaterializerMethodOptimizer Optimizer {
get { return this.optimizer; }
}
}
/// <summary>
/// Interface method optimizers must implement to be used by the Materializer component.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Materializer")]
public interface IMaterializerMethodOptimizer {
/// <summary>
/// Optimize a method call.
/// </summary>
/// <remarks>
/// Implementations should return the input expression if they are unable to optimize
/// rather than throwing or returning null.
/// </remarks>
/// <param name="fieldNames">Names and order of fields available in the given record.</param>
/// <param name="recordParameter">Record parameter (of type IDataRecord).</param>
/// <param name="methodCall">Expression representing the method call.</param>
/// <returns>Optimized method call.</returns>
Expression OptimizeMethodCall(ReadOnlyCollection<string> fieldNames, ParameterExpression recordParameter, MethodCallExpression methodCall);
}
}
|