2013-10-28 3 views
1

이를 사용하여 C#에서 람다 쿼리의 상당 부분을 교체하는 방법을 생각합니다. 지원되는 언어의 양이 늘어날 수 있기 때문에 모든 제품에는 Culture (예 : 'en-US')와 모든 번역 가능한 속성이 포함 된 ProductTranslation 컬렉션이 있습니다.엔티티 프레임 워크 :</p> <p>우리의 응용 프로그램은 제품의 테이블을 가지고, 번역 이름을 가지고 : ExpressionVisitor

도메인은 다음과 같습니다

/// <summary> 
///  Marks a class as translatable, i.e. there are properties that need to be different per language, such as a name or description. 
/// </summary> 
/// <typeparam name="TTranslation"></typeparam> 
public interface ITranslatable<TTranslation> where TTranslation: ITranslation 
{ 
    /// <summary> 
    ///  Gets or sets the translations 
    /// </summary> 
    TranslationCollection<TTranslation> Translations { get; set; } 
} 

/// <summary> 
/// Marks this class as a translation of another class. 
/// </summary> 
public interface ITranslation 
{ 
    /// <summary> 
    ///  Gets or sets the culture 
    /// </summary> 
    string Culture { get; set; } 
} 

public class Product : ITranslatable<ProductTranslation> 
{ 
    private TranslationCollection<ProductTranslation> _translations; 

    /// <summary> 
    ///  Gets or sets the code. 
    /// </summary> 
    public virtual string Code { get; set; } 

    /// <summary> 
    ///  Gets or sets the price. 
    /// </summary> 
    public virtual decimal? Price { get; set; } 

    public virtual TranslationCollection<ProductTranslation> Translations 
    { 
     get { return _translations ?? (_translations = new TranslationCollection<ProductTranslation>()); } 
     set { _translations = value; } 
    } 
} 

/// <summary> 
///  Contains the translatable properties for <see cref="Product"/> 
/// </summary> 
public class ProductTranslation: ITranslation 
{ 
    /// <summary> 
    ///  Gets or sets the culture of this translation 
    /// </summary> 
    public string Culture { get; set; } 

    /// <summary> 
    ///  Gets or sets the name. 
    /// </summary> 
    public virtual string Name { get; set; } 
} 
당신이다시피

, 내가 번역에 대한 사용자 지정 컬렉션 클래스를 사용하고 있습니다. (대신 기본은 ICollection의 TranslationCollection가)

이 클래스는 컬렉션을 확장하지만 현재의 UI 문화와 일치하는 번역 반환 '현재'유틸리티 속성을 추가합니다

/// <summary> 
///  Contains specific methods to work with translations 
/// </summary> 
/// <typeparam name="TTranslation"></typeparam> 
public class TranslationCollection<TTranslation>: Collection<TTranslation> where TTranslation: ITranslation 
{ 
    /// <summary> 
    ///  Initializes an empty <see cref="TranslationCollection{TTranslation}"/> 
    /// </summary> 
    public TranslationCollection() 
    { 
    } 

    /// <summary> 
    ///  Initializes a new <see cref="TranslationCollection{TTranslation}"/> with the given <paramref name="list"/> as its contents 
    /// </summary> 
    /// <param name="list"></param> 
    public TranslationCollection(IList<TTranslation> list) : base(list) 
    { 
    } 

    /// <summary> 
    ///  Returns the translation that has the same culture as the current UI culture. 
    /// </summary> 
    public TTranslation Current 
    { 
     get 
     { 
      return this.SingleOrDefault(t => t.Culture == CultureInfo.CurrentUICulture.Name); 
     } 
    } 
} 

당신이 볼 수 있듯이을 거의있다 여기에 있지만, 나중에 사용자 정의 컬렉션 클래스가 필요하다는 생각이 듭니다. 나중에 표시 및 양식을위한 사용자 정의 HTML 구성 요소를 만들고 싶을 때 유용 할 수 있습니다.

이제 질문 : 우리는 제품 테이블을 쿼리 할 때

, 그 이름으로 검색은 다음과 같이 보일 것입니다 :

var products = dbContext.Products.Where(p => p.Translations.Where(t => t.Culture == CultureInfo.CurrentUICulture).Any(t => t.Name.ToLower().Contains("abc"))) 

그러나,로 보는 것은 번역의 많은있을 것 테이블 (앞으로는 상당히 큰 응용 프로그램입니다.)을 작성하면 매우 흥미로울 것입니다.

var products = dbContext.Products.Where(p => p.Translations.Current.Name.ToLower().Contains("abc")) 

이 코드를 실행하면 '현재'속성의 매핑이 해제되고 Entity Framework에서 예외가 throw됩니다. 그러나, 자동으로 나는 첫 번째 시도를했습니다 ExpressionVisitor (또는 다른 것)

를 사용하여, 뭔가 다른 '현재'전화를 변환하는 것이 가능하지만 조금 붙어 것 :

public class CurrentTranslationVisitor: ExpressionVisitor 
{ 
    protected override Expression VisitMember(MemberExpression node) 
    { 
     if(node.Member.MemberType != MemberTypes.Property) 
      return base.VisitMember(node); 
     var propertyInfo = node.Member as PropertyInfo; 
     if (propertyInfo == null) 
      return base.VisitMember(node); 
     if (!typeof (ITranslation).IsAssignableFrom(propertyInfo.PropertyType)) 
      return base.VisitMember(node); 
     if (!string.Equals(propertyInfo.Name, "Current")) 
      return base.VisitMember(node); 

     // at this point we can be confident that the property is [someTranslatableObject].Translations.Current 

    } 
} 

현재 시점에서 Current 속성에 대해 작성된 코드에 어떻게 액세스합니까? 표현은 내가

.Name.ToLower().Contains("abc") 

제안 및 지원에 액세스 할 수있는 방법을

p => p.Translations.Current.Name.ToLower().Contains("abc") 

입니다

예를 들어

은 많이 주시면 감사하겠습니다!

답변

1

그래서 시작하기 위해 다음 도우미 메서드를 사용하여 표현식을 결합합니다. 표현을 외부에서 볼 수있는 구성없이 구성 할 수 있습니다. 이 Compose 메서드는 LambadExpression을 사용하고 다른 사람의 입력은 첫 번째 출력의 것과 동일한 유형입니다. 이것이 함수라면 우리는 단지 하나를 호출하고 결과를 다른 것의 입력으로 전달할 것입니다. 이것들은 표현식이기 때문에 그보다 좀 더 복잡합니다. 표현식 방문자를 사용하여 매개 변수의 모든 인스턴스를 다른 매개 변수의 본문으로 대체해야합니다.

이 도우미 함수가 필요로하는 방문자 : 방법 자체

public class ReplaceVisitor : ExpressionVisitor 
{ 
    private readonly Expression from, to; 
    public ReplaceVisitor(Expression from, Expression to) 
    { 
     this.from = from; 
     this.to = to; 
    } 
    public override Expression Visit(Expression node) 
    { 
     return node == from ? to : base.Visit(node); 
    } 
} 

이 지금 우리가 만들려면이 Compose 방법을 사용할 수 있습니다 this previous answer of mine

에서 가져온 것이

public static Expression<Func<TFirstParam, TResult>> 
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first, 
    Expression<Func<TIntermediate, TResult>> second) 
{ 
    var param = Expression.Parameter(typeof(TFirstParam), "param"); 

    var newFirst = new ReplaceVisitor(first.Parameters.First(), param) 
     .Visit(first.Body); 
    var newSecond = new ReplaceVisitor(second.Parameters.First(), newFirst) 
     .Visit(second.Body); 

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param); 
} 

LambdaExpressionTranslationCollection으로 가져오고 그 중 LambdaExpression을 반환하는 방법 현재 문화권을 나타내는 단일 ITranslation 개체에 매핑 된 동일한 개체입니다. 이 시점에서 대부분의 작업은 이미 완료되었습니다.

public static Expression<Func<T, TTranslation>> SelectCurrent<T, TTranslation> 
    (Expression<Func<T, TranslationCollection<TTranslation>>> expression) 
    where TTranslation : ITranslation 
{ 
    return expression.Compose(collection => 
     collection.FirstOrDefault(t => t.Culture == CultureInfo.CurrentUICulture.Name)); 
} 

예제 사용 예를 이제 보겠습니다. 우리는 우리가 적용 할 실제 필터에 그 번역을지도하기 위해 다음 Compose을 현재 번역을 얻을 SelectCurrent를 사용하는 제품을 쿼리를 수행 할 수 있습니다

public static void Foo() 
{ 
    IQueryable<Product> products = null; 
    var query = products.Where(
     SelectCurrent((Product p) => p.Translations) 
     .Compose(translation => translation.Name.ToLower().Contains("abc"))); 
} 
+0

아주 재미있는 물건을! 나는 내일 아침에 너에게 먼저 돌아갈거야, 내 머리는 오늘 표정 나무로 깊숙히 뛰어 드는 것에서 조금 흐려있다. :-) 어쨌든 당신의 대답에 감사드립니다! – Moeri

+0

여러분의 아이디어를 완벽하게 구현하지는 못했지만 올바른 방식은 번역 속성 선택기의 표현식과 논리가 포함 된 표현식을 분리하는 것입니다. 필자는 작성 기능 대신 LinqKit을 사용했는데 (필자의 작성 패러다임보다 뛰어나고 확장하는 아이디어가 맘에 든다.) 나는 이렇게 사용자 정의 필터 클래스에 전달한다 : productFilter.Where (p => p.Translations, pt => pt.Name.Contains ("abc")) 올바른 경로에 나를 설정해 주셔서 감사합니다! – Moeri

관련 문제