2014-09-29 95 views
1

我試圖重構遍佈各處使用的一行代碼。我們正在使用EF6.1並希望找到電話和電子郵件(以字符串形式)。C#Linq to Entities方法將屬性投影到字符串

public SiteLabelDto[] GetVendorSites(int vendorId) 
    { 
     return Context.Sites.Where(s => !s.IsDeleted && s.Vendor.Id == vendorId) 
      .Select(s => new SiteLabelDto 
      { 
       Id = s.Id, 
       Name = s.Name,      
       Address = s.Address.StreetAddress + ", " + s.Address.Locality + ", " + s.Address.Region + ", " + s.Address.Postcode, 
       Country = s.Address.Country.Name, 
       Email = s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Email) != null ? s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Email).Value : "", 
       Phone = s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Phone) != null ? s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Phone).Value : "",      
      }).ToArray(); 
    } 

上面的代碼獲取了接觸點列表,並試圖找到最適合每種類型的代碼。

public class ContactPointEntity 
{ 
    public int Id { get; set; } 
    public string Value { get; set; } 
    public ContactPointType Type { get; set; } 
    public bool IsDefault { get; set; } 
} 

該方法將被擴展爲嘗試返回第一個IsDefault。

我的目標是嘗試並能夠投入的方法或擴展,這樣我可以說s.GetcontactPoint(ContactPointType.Email)或s.contactPoints.GetPoints(ContactPointType.Email)並返回字符串值,或者如果字符串不是可能的情況,則返回聯繫點類。

我越讀它,我想我需要建立一些表達式樹,不知道現在怎麼樣。

回答

2

你需要建立一個表達式樹。

首先,因爲你需要引入IsDefault條件下,表達可以是這樣的:

s.ContactPoints 
.Where(x => x.Type == ContactPointType.Email && x.IsDefault) 
.Select(x => x.Value) 
.DefaultIfEmpty(string.Empty) 
.FirstOrDefault() 

然後,接觸點選擇轉換成一種表達。

private static Expression<Func<Site, string>> GetContactPoint(ParameterExpression siteParam, ParameterExpression cpeParam, ContactPointType type) 
{ 
    // Where. 
    var typeCondition = Expression.Equal(Expression.Property(cpeParam, "Type"), Expression.Constant(type)); 
    var defaultCondition = Expression.Equal(Expression.Property(cpeParam, "IsDefault"), Expression.Constant(true)); 
    var condition = Expression.AndAlso(typeCondition, defaultCondition); 
    var predicateExp = Expression.Lambda<Func<ContactPointEntity, bool>>(condition, cpeParam); 
    var whereExp = Expression.Call(typeof(Enumerable), "Where", new[] { typeof(ContactPointEntity) }, Expression.Property(siteParam, "ContactPoints"), predicateExp); 

    // Select. 
    var valueExp = Expression.Lambda<Func<ContactPointEntity, string>>(Expression.Property(cpeParam, "Value"), cpeParam); 
    var selectExp = Expression.Call(typeof(Enumerable), "Select", new[] { typeof(ContactPointEntity), typeof(string) }, whereExp, valueExp); 

    // DefaultIfEmpty. 
    var defaultIfEmptyExp = Expression.Call(typeof(Enumerable), "DefaultIfEmpty", new[] { typeof(string) }, selectExp, Expression.Constant(string.Empty)); 

    // FirstOrDefault. 
    var firstOrDefaultExp = Expression.Call(typeof(Enumerable), "FirstOrDefault", new[] { typeof(string) }, defaultIfEmptyExp); 

    var selector = Expression.Lambda<Func<Site, string>>(firstOrDefaultExp, siteParam); 
    return selector; 
} 

並且還創建站點標籤dto選擇器。

private static Expression<Func<Site, SiteLabelDto>> GetSite(ParameterExpression siteParam, ParameterExpression cpeParam) 
{ 
    var newExp = Expression.New(typeof(SiteLabelDto)); 
    var initExp = Expression.MemberInit(
     newExp, 
     Expression.Bind(typeof(SiteLabelDto).GetProperty("Id"), Expression.Lambda<Func<Site, int>>(Expression.Property(siteParam, "Id"), siteParam).Body), 
     Expression.Bind(typeof(SiteLabelDto).GetProperty("Name"), Expression.Lambda<Func<Site, string>>(Expression.Property(siteParam, "Name"), siteParam).Body), 
     /* other basic information */ 
     Expression.Bind(typeof(SiteLabelDto).GetProperty("Email"), GetContactPoint(siteParam, cpeParam, ContactPointType.Email).Body), 
     Expression.Bind(typeof(SiteLabelDto).GetProperty("Phone"), GetContactPoint(siteParam, cpeParam, ContactPointType.Phone).Body) 
     /* other types */ 
    ); 
    var selector = Expression.Lambda<Func<Site, SiteLabelDto>>(initExp, siteParam); 
    return selector; 
} 

用法。

var siteParam = Expression.Parameter(typeof(Site), "s"); 
var cpeParam = Expression.Parameter(typeof(ContactPointEntity), "cpe"); 
var selector = GetSite(siteParam, cpeParam); 
return Context.Sites 
    .Where(s => !s.IsDeleted && s.Vendor.Id == vendorId) 
    .Select(selector) 
    .ToArray(); 

PS:

也許有些上面需要的代碼進行重構,這只是給出了基本思路是如何做到這一點。

更新

你也可以創建一個包裝類與所有接觸點一起包含EF實例。

public class ContactPointExt<T> 
{ 
    public T Instance { get; set; } 
    public string Email { get; set; } 
    public string Phone { get; set; } 
} 

然後改變GetSiteGetContactPoints返回結果爲ContactPointExt<T>

private static Expression<Func<Site, ContactPointExt<T>>> GetContactPoints<T>(ParameterExpression siteParam, ParameterExpression cpeParam) 
{ 
    var type = typeof(ContactPointExt<T>); 
    var newExp = Expression.New(type); 
    var initExp = Expression.MemberInit(
     newExp, 
     Expression.Bind(type.GetProperty("Instance"), siteParam), 
     Expression.Bind(type.GetProperty("Email"), GetContactPoint(siteParam, cpeParam, ContactPointType.Email).Body), 
     Expression.Bind(type.GetProperty("Phone"), GetContactPoint(siteParam, cpeParam, ContactPointType.Phone).Body) 
    ); 
    var selector = Expression.Lambda<Func<Site, ContactPointExt<T>>>(initExp, siteParam); 
    return selector; 
} 

ContactPointExt<T>結果可以被重新投影到SiteLabelDto與另一Select。從OP

var siteParam = Expression.Parameter(typeof(Site), "s"); 
var cpeParam = Expression.Parameter(typeof(ContactPointEntity), "cpe"); 
var selector = GetContactPoints<Site>(siteParam, cpeParam); 
return Context.Sites 
    .Where(s => !s.IsDeleted && s.Vendor.Id == vendorId) 
    .Select(selector) 
    .Select(s => new SiteLabelDto 
    { 
     Id = s.Instance.Id, 
     Name = s.Instance.Name, 
     Email = s.Email, 
     Phone = s.Phone 
    }) 
    .ToArray(); 

EDIT我們創建了一個包裝方法,使這個一點點簡單的重複使用,把它在這裏只是爲了證明別人:

/// <summary> 
    /// Wraps up a each of a query's objects in a ContactPointExt&lt;T&gt; object, providing the default contact point of each type. 
    /// The original query object is accessed via the "Instance" property on each result. 
    /// Assumes that the query object type has a property called ContactPoints - if different, supply the property name as the first argument. 
    /// </summary> 
    /// <typeparam name="T"></typeparam> 
    /// <param name="query"></param> 
    /// <param name="contactPointsPropertyName"></param> 
    /// <returns></returns> 
    public static IQueryable<ContactPointExt<T>> WithContactPointProcessing<T>(this IQueryable<T> query, string contactPointsPropertyName = "ContactPoints") where T : class 
    { 
     var siteParam = Expression.Parameter(typeof(T), "s"); 
     var cpeParam = Expression.Parameter(typeof(ContactPointEntity), "cpe"); 
     var selector = ContactPointHelpers.GetContactPoints<T>(siteParam, cpeParam, contactPointsPropertyName); 
     return query.Select(selector); 
    } 
+0

我確實喜歡你的答案,但我需要在接觸點上這樣做,以便它可以重複使用,而不必將網站作爲表達樹來使用,因爲這意味着必須做很多我們的回購層作爲表達髮束。但答案很接近。我不確定是否可以「重新調整」getcontactpoint表達式方法以便在接觸點上使用,因爲如何在這一點上將選擇表達式傳遞給它們。我會認爲,如果我做了s.contactpoints.select(exp())它只能在單個接觸點上工作,而不是在整個列表中。 – Jon 2014-09-30 08:30:20

+0

@Jon,看到我的更新,我創建了另一個擴展,以便聯繫點可以重用(並重新投影到另一個類) – 2014-09-30 11:28:40

+1

謝謝。這很棒,就像一個魅力..我已經編輯了你的答案,以顯示我們寫的幫手,如果其他人遇到這個問題,它會多一點。再次感謝 – Jon 2014-09-30 14:49:12

0

對Linq-to-entites使用擴展方法有點棘手,因爲並不是所有提供者都能理解並轉化爲相應的後端調用。一個相對安全的辦法是採取IQueryable並返回IQueryable它可以解決:

public static IQueryable<SiteDTO> MapToSiteDTO(this IQueryable<Site> query) 
{ 
    return query.Select(s => new SiteLabelDto 
          { 
           Id = s.Id, 
           Name = s.Name,      
           Address = s.Address.StreetAddress + ", " + s.Address.Locality + ", " + s.Address.Region + ", " + s.Address.Postcode, 
           Country = s.Address.Country.Name, 
           Email = s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Email) != null ? s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Email).Value : "", 
           Phone = s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Phone) != null ? s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Phone).Value : "",      
          }); 
} 

然後你把它想:

return Context.Sites.Where(s => !s.IsDeleted && s.Vendor.Id == vendorId) 
      .Select(x => x) 
      .MapToSiteDTO() 
      .ToArray(); 
+0

Mrchief嗨,這一切編譯但我得到:INQ實體不識別方法'System.String GetContactPoint(System.Collections.Generic.IEnumerable'1 [Opus.Repository.Models.ContactPointEntity],Opus.Repository.Models.ContactPointType)'方法,和此方法無法轉換爲商店表達。 – Jon 2014-09-29 16:02:28

+0

也認爲有一個輸入錯誤,你檢查src是否爲空。我認爲你的意思是......沒有正確的意思,我很想理解它。謝謝 – Jon 2014-09-29 16:03:17

+0

更新了我的答案。如果您試圖重複您的映射代碼,那麼這種方法將會很好。或者,如果您正在尋找一種通用的方法來消除此處的ContactPointType查詢以及其他查詢,那麼您可能需要對此進行調整。 – Mrchief 2014-09-29 17:05:25

0
public SiteLabelDto[] GetVendorSites(int vendorId) 
{ 
    return (from s in Context.Sites 
     where !s.IsDeleted && s.Vendor.Id == vendorId 
     let email = s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Email) 
     let phone = s.ContactPoints.FirstOrDefault(x => x.Type == ContactPointType.Phone) 
     select new SiteLabelDto 
     { 
      Id = s.Id, 
      Name = s.Name,      
      Address = s.Address.StreetAddress + ", " + s.Address.Locality + ", " + s.Address.Region + ", " + s.Address.Postcode, 
      Country = s.Address.Country.Name, 
      Email = email != null ? email.Value : "", 
      Phone = phone != null ? phone .Value : "", 
     }).ToArray(); 
} 
+0

嗨,詹姆斯,我的目標是控制接觸點在整個域內以及跨越多個不同POCO的各個位置出現的方式,而不是如上所述明確使用它,因爲需要多次重寫該代碼。問候 – Jon 2014-09-29 19:11:36