Практически любой .NET разработчик так или иначе использует в своей практике технологию Linq. Linq позволяет писать красивый и лаконичный код для получения объектов из источника данных с возможностью определения критериев получения и/или трансформации запрошенных объектов «на лету». Поддержка Linq присутствует практически во всех популярных ORM-фреймворках, в том числе и в NHibernate. NHibernate предоставляет Linq-провайдер, с помощью которого мы можем написать запрос на этапе разработки (Design-Time), но для того, чтобы составить запрос в runtime, придется повозиться с Reflection. Однако, если возникнет потребность в формировании запроса во внешнем процессе, например, в клиентской части сервиса, то в таком случае Reflection уже не спасет, клиентская часть, как правило, не знает (и не должна ничего знать) про серверный ORM.
Ниже мы разберем как создать API для написания Linq запросов к NHibernate в ситуации, когда запрос пишется в одном процессе, а выполняется в другом. Также, реализуем собственный IQueryProvider, который будет транслировать запросы из приложения-источника в исполняющее приложение.
// List ctor
public List<T>(IEnumerable<T> source)
{
// получаем IEnumerator последнего WhereListIterator.
var enumerator = source.GetEnumerator();
// в этот момент происходит вызов всей цепочки WhereListIterator'ов для получения следующего элемента с учетом фильтра
while(enumerator.MoveNext())
{
items.Add(enumerator.Current)
}
}
public List<T>(IQueryable<T> source)
{
// получаем IEnumerator последнего WhereListIterator.
var enumerator = source.GetEnumerator();
// в этот момент происходит вызов всей цепочки WhereListIterator'ов для получения следующего элемента с учетом фильтра
while(enumerator.MoveNext())
{
items.Add(enumerator.Current)
}
}
public class EnumerableQueryable<T> : IQueryable<T>
{
private Expression expression;
private IEnumerable enumerableResult;
public IEnumerator GetEnumerator()
{
if (enumerableResult == null)
enumerableResult = expression.Compile().Invoke();
return enumerableResult.GetEnumerator();
}
}
var activeMasterEntities = session
// вернет NhQueryable<T>, с ConstantExpression внутри, замкнутым на самого себя в качестве источника данных.
.Query<Entity>()
// вернет IQueryable, с MethodCallExpression внутри, который декорирует ConstantExpression.
.Where(e => e.IsMaster == true)
// вернет IQueryable, с MethodCallExpression внутри, который декорирует первый MethodCallExpression.
.Where(e => e.IsActive == true)
// запустит выполнение запроса
.ToList()
int stateCoefficient = 0.9;
int ageLimitInCurrentState = 18 * stateCoefficient;
var availableMovies = session
.Query<Movie>()
.Where(m => m.AgeLimit >= ageLimitInCurrentState)
.ToList()
public class NhQueryable<T> : QueryableBase<T>
{
public NhQueryable(ISessionImplementor session)
{
// Создаем провайдер с типом INhQueryProvider
Provider = QueryProviderFactory.CreateQueryProvider(session);
// источником данных устанавливаем текущий объект.
Expression = Expression.Constant(this);
}
}
public class RemoteQueryable<T> : IQueryable<T>
{
public Expression Expression { get; set; }
public Type ElementType { get; set; }
public IQueryProvider Provider { get; set; }
public RemoteQueryable()
{
Expression = Expression.Constant(this);
}
}
var query = new RemoteQueryable<Entity>().Where(e => e.IsMaster);
public static class RemoteRepository
{
public static IQueryable<TResult> CreateQuery<TResult>(IChannelProvider provider)
{
return new RemoteQueryable<TResult>(provider);
}
}
var query = RemoteRepository.CreateQuery<Entity>()
.Where(e => e.IsMaster)
.Where(e => e.IsActive);
public class NhibernateExpressionVisitor : ExpressionVisitor
{
protected IQueryable queryableRoot;
public new Expression Visit(Expression sourceExpression, IQueryable queryableRoot)
{
this.queryableRoot= queryableRoot;
return Visit(sourceExpression);
}
protected override Expression VisitMethodCall(MethodCallExpression m)
{
var query = m;
var constantArgument = query.Arguments.FirstOrDefault(e => e is ConstantExpression && e.Type.IsGenericType && e.Type.GetGenericTypeDefinition() == typeof(EnumerableQuery<>));
if (constantArgument != null)
{
var constantArgumentPosition = query.Arguments.IndexOf(constantArgument);
var newArguments = new Expression[query.Arguments.Count];
for (int index = 0; index < newArguments.Length; index++)
{
if (index != constantArgumentPosition)
newArguments[index] = query.Arguments[index];
else
newArguments[index] = queryableRoot.Expression;
}
return Expression.Call(query.Object, query.Method, newArguments);
}
return base.VisitMethodCall(query);
}
protected override Expression VisitConstant(ConstantExpression c)
{
if (c.Type.IsGenericType && typeof(RemoteQueryable<>).IsAssignableFrom(c.Type.GetGenericTypeDefinition()))
return queryableRoot.Expression;
return c;
}
}
var query = RemoteRepository.CreateQuery<Entity>()
.Where(e => e.IsMaster)
.Where(e => e.IsActive);
using (var session = CreateSession())
{
var nhQueryable = session.Query<Entity>();
var nhQueryableWithExternalQuery = new NhibernateExpressionVisitor().Visit(query.Expression, nhQueryable);
var result = nhQueryable.Provider.Execute(nhQueryableWithExternalQuery);
}
public interface IChannelProvider
{
T SendRequest<T>(string request);
}
public RemoteQueryable(IChannelProvider channelProvider)
{
Expression = Expression.Constant(this);
Provider = new RemoteQueryableProvider<T>(channelProvider);
}
public static IQueryable<TResult> CreateQuery<TResult>(IChannelProvider provider)
{
return new RemoteQueryable<TResult>(provider);
}
public class RemoteQueryProvider : IQueryProvider
{
public IQueryable CreateQuery(Expression expression)
{
var enumerableQuery = new EnumerableQuery<T>(expression);
var resultQueryable = ((IQueryProvider)enumerableQuery).CreateQuery(expression);
return new RemoteQueryable<T>(this, resultQueryable.Expression);
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
var enumerableQuery = new EnumerableQuery<TElement>(expression);
var resultQueryable = ((IQueryProvider)enumerableQuery).CreateQuery<TElement>(expression);
return new RemoteQueryable<TElement>(this, resultQueryable.Expression);
}
public object Execute(Expression expression)
{
var serializedQuery = SerializeQuery(expression);
return channelProvider.SendRequest<object>(serializedQuery);
}
public TResult Execute<TResult>(Expression expression)
{
var serializedQuery = SerializeQuery(expression);
return this.channelProvider.SendRequest<TResult>(serializedQuery);
}
public RemoteQueryableProvider(IChannelProvider channelProvider)
{
this.channelProvider = channelProvider;
}
private static string SerializeQuery(Expression expression)
{
var newQueryDto = QueryDto.CreateMessage(expression, typeof(T));
var serializedQuery = JsonConvert.SerializeObject(newQueryDto, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.All
});
return serializedQuery;
}
}
public class QueryDto
{
public ExpressionNode SerializedExpression { get; set; }
public string RequestedTypeName { get; set; }
public string RequestedTypeAssemblyName { get; set; }
public static QueryDtoCreateMessage(Expression expression, Type type)
{
var serializedExpression = expression.ToExpressionNode();
return new QueryDto(serializedExpression, type.FullName, type.Assembly.FullName);
}
private QueryDto(ExpressionNode serializedExpression, string requestedTypeName, string requestedTypeAssemblyName)
{
this.SerializedExpression = serializedExpression;
this.RequestedTypeName = requestedTypeName;
this.RequestedTypeAssemblyName = requestedTypeAssemblyName;
}
protected QueryDto() { }
}
RemoteRepository.Query<WorkItem>()
.Where(w => w.Priority == EnvironmentSettings.MaxPriority)) // ссылка на EnvironmentSettings
internal class ClientExpressionVisitor : ExpressionVisitor
{
public Expression Evaluate(Expression expression)
{
return base.Visit(expression);
}
private Expression EvaluateIfNeed(Expression expression)
{
var memberExpression = expression as MemberExpression;
if (memberExpression != null)
{
if (memberExpression.Expression is ParameterExpression)
return expression;
var rightValue = GetValue(memberExpression);
return Expression.Constant(rightValue);
}
var methodCallExpression = expression as MethodCallExpression;
if (methodCallExpression != null)
{
var obj = ((ConstantExpression)methodCallExpression.Object).Value;
var result = methodCallExpression.Method.Invoke(obj,
methodCallExpression.Arguments.Select(ResolveArgument).ToArray());
return Expression.Constant(result);
}
return expression;
}
protected override Expression VisitBinary(BinaryExpression b)
{
Expression left = this.EvaluateIfNeed(this.Visit(b.Left));
Expression right = this.EvaluateIfNeed(this.Visit(b.Right));
Expression conversion = this.Visit(b.Conversion);
if (left != b.Left || right != b.Right || conversion != b.Conversion)
{
if (b.NodeType == ExpressionType.Coalesce && b.Conversion != null)
return Expression.Coalesce(left, right, conversion as LambdaExpression);
else
return Expression.MakeBinary(b.NodeType, left, right, b.IsLiftedToNull, b.Method);
}
return b;
}
private static object ResolveArgument(Expression exp)
{
var constantExp = exp as ConstantExpression;
if (constantExp != null)
return constantExp.Value;
var memberExp = exp as MemberExpression;
if (memberExp != null)
return GetValue(memberExp);
return null;
}
private static object GetValue(MemberExpression exp)
{
var constantExpression = exp.Expression as ConstantExpression;
if (constantExpression != null)
{
var member = constantExpression.Value
.GetType()
.GetMember(exp.Member.Name)
.First();
var fieldInfo = member as FieldInfo;
if (fieldInfo != null)
return fieldInfo.GetValue(constantExpression.Value);
var propertyInfo = member as PropertyInfo;
if (propertyInfo != null)
return propertyInfo.GetValue(constantExpression.Value);
}
var expression = exp.Expression as MemberExpression;
if (expression != null)
return GetValue(expression);
return null;
}
}
public object Execute(Expression expression)
{
var partialEvaluatedExpression = this.expressionEvaluator.Evaluate(expression);
var serializedQuery = SerializeQuery(partialEvaluatedExpression);
return channelProvider.SendRequest<object>(serializedQuery);
}
public TResult Execute<TResult>(Expression expression)
{
var partialEvaluatedExpression = this.expressionEvaluator.Evaluate(expression);
var serializedQuery = SerializeQuery(partialEvaluatedExpression);
return this.channelProvider.SendRequest<TResult>(serializedQuery);
}
internal class PostQueryable<T> : BaseQueryable<T>
{
public PostQueryable(IChannelProvider channelProvider) : base(channelProvider) { }
public PostQueryable(AbstractQueryProvider provider, Expression expression) : base(provider, expression) { }
public PostQueryable() { Expression = Expression.Constant(this); }
}
public static class Ex
{
public static IQueryable<T> PostQuery<T>(this IQueryable<T> sourceQuery)
{
var query = Expression
.Call(null, typeof (PostQueryable<T>).GetMethod(nameof(PostQueryable<T>.WrapQuery)), new [] {sourceQuery.Expression});
return sourceQuery.Provider.CreateQuery<T>(query);
}
}
int stateCoefficient = 0.9;
int ageLimitInCurrentState = 18 * stateCoefficient;
var availableMovies = session
.Query<Movie>()
.Where(m => m.AgeLimit >= ageLimitInCurrentState)
.PostQuery()
.Where(m => m.RatingInCurrentState > 8) // unmapped-свойство RatingInCurrentState
.ToList()
internal static class NHibernateTypesHelper
{
private static readonly Assembly nhibernateAssembly;
public static Type SessionType { get; private set; }
public static Type LinqExtensionType { get; private set; }
public static bool IsSessionObject(object inspectedObject)
{
return SessionType.IsInstanceOfType(inspectedObject);
}
static NHibernateTypesHelper()
{
nhibernateAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(asm => asm.FullName.Contains("NHibernate")) ?? Assembly.Load("NHibernate");
if (nhibernateAssembly == null)
throw new InvalidOperationException("Caller invoking server-side types, but the NHibernate.dll not found in current application domain");
SessionType = nhibernateAssembly.GetTypes()
.Single(p => p.FullName.Equals("NHibernate.ISession", StringComparison.OrdinalIgnoreCase));
LinqExtensionType = nhibernateAssembly.GetTypes()
Single(p => p.FullName.Equals("NHibernate.Linq.LinqExtensionMethods", StringComparison.OrdinalIgnoreCase));
}
}
public static class RemoteQueryExecutor
{
public static object Do(string serializedQueryDto, object sessionObject)
{
var internalRemoteQuery = DeserializeQueryDto(serializedQueryDto);
var deserializedQuery = DeserializedQueryExpressionAndValidate(internalRemoteQuery);
var targetType = ResolveType(internalRemoteQuery);
return Execute(deserializedQuery, targetType, sessionObject);
}
private static TypeInfo ResolveType(QueryDto internalRemoteQuery)
{
var targetAssemblyName = internalRemoteQuery.RequestedTypeAssemblyName;
var targetAssembly = GetAssemblyOrThrownEx(internalRemoteQuery, targetAssemblyName);
var targetType = GetTypeFromAssemblyOrThrownEx(targetAssembly, internalRemoteQuery.RequestedTypeName,
targetAssemblyName);
return targetType;
}
private static Expression DeserializedQueryExpression(QueryDto internalRemoteQuery)
{
var deserializedQuery = internalRemoteQuery.SerializedExpression.ToExpression();
return deserializedQuery;
}
private static TypeInfo GetTypeFromAssemblyOrThrownEx(Assembly targetAssembly, string requestedTypeName, string targetAssemblyName)
{
var targetType = targetAssembly.DefinedTypes
.FirstOrDefault(type => type.FullName.Equals(requestedTypeName, StringComparison.OrdinalIgnoreCase));
if (targetType == null)
throw new InvalidOperationException(string.Format("Type with name '{0}' not found in assembly '{1}'", requestedTypeName, targetAssemblyName));
return targetType;
}
private static Assembly GetAssemblyOrThrownEx(QueryDto internalRemoteQuery, string targetAssemblyName)
{
var targetAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(asm => asm.FullName.Equals(internalRemoteQuery.RequestedTypeAssemblyName, StringComparison.OrdinalIgnoreCase));
if (targetAssembly == null)
throw new InvalidOperationException(string.Format("Assembly with name '{0}' not found in server app domain", targetAssemblyName));
return targetAssembly;
}
private static QueryDto DeserializeQueryDto(string serializedQueryDto)
{
var internalRemoteQuery = JsonConvert
.DeserializeObject<QueryDto>(serializedQueryDto, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.All
});
return internalRemoteQuery;
}
private static object Execute(Expression expression, Type targetType, object sessionObject)
{
var queryable = GetNhQueryableFromSession(targetType, sessionObject);
var nhibernatePartialExpression = ExpressionModifier.GetNhibernatePartialExpression(expression, queryable);
var resultFromStorage = queryable.Provider.Execute(nhibernatePartialExpression);
var requestedCollection = resultFromStorage as IEnumerable<object>;
if (requestedCollection == null)
return resultFromStorage;
var resultCollectionType = requestedCollection.GetType();
if (resultCollectionType.IsGenericType)
targetType = resultCollectionType.GetGenericArguments().Single();
var enumerableQueryable = (IQueryable)Activator
.CreateInstance(typeof(EnumerableQuery<>).MakeGenericType(targetType), new[] { requestedCollection });
var postQueryPartialExpression = ExpressionModifier
.GetPostQueryPartialExpression(expression, enumerableQueryable);
if (postQueryPartialExpression == null)
return resultFromStorage;
return enumerableQueryable.Provider.Execute(postQueryPartialExpression);
}
private static IQueryable GetNhQueryableFromSession(Type targetType, object sessionObject)
{
var finalQueryMethod = ResolveQueryMethod(targetType);
var queryable = (IQueryable) finalQueryMethod.Invoke(null, new object[] {sessionObject});
return queryable;
}
private static MethodInfo ResolveQueryMethod(Type targetType)
{
var queryMethod = typeof(LinqExtensionMethods).GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.IsGenericMethod)
.Where(m => m.Name.Equals("Query"))
.Single(m => m.GetParameters().Length == 1 && NHibernateTypesHelper.SessionType.IsAssignableFrom(m.GetParameters().First().ParameterType));
var finalQueryMethod = queryMethod.MakeGenericMethod(targetType);
return finalQueryMethod;
}
}
[DataContract]
public class WorkItem : BaseEntity
{
[DataMember]
public virtual string Text { get; set; }
[DataMember]
public virtual int Priority { get; set; }
}
public class DemoWorkItemProvider : IItemsProvider<WorkItem>
{
public int FetchCount()
{
return RemoteRepository.CreateQuery<WorkItem>(new DemoChannelProvider())
.Count();
}
public IList<WorkItem> FetchRange(int startIndex, int count)
{
return RemoteRepository.CreateQuery<WorkItem>(new DemoChannelProvider())
.Skip(startIndex)
.Take(count)
.ToList();
}
}
К сожалению, не доступен сервер mySQL