ไวยากรณ์ที่คล่องแคล่วสำหรับวิธีการขยาย IQueryable หรือไม่

ฉันจะปล่อยให้โค้ดพูดเพื่อตัวมันเอง:

public interface ISoftDeletable {
  bool IsDeleted {get; set;}
}

public static class Extensions {
  public IQueryable<T> Active<T>(this IQueryable<T> q) where T : ISoftDeletable {
    return q.Where(t => !t.IsDeleted);
  }
}

public partial class Thing : ISoftDeletable {
  ...
}

...
var query = from tc in db.ThingContainers
            where tc.Things.Active().Any(t => t.SatisfiesOtherCondition)
            select new { ... }; // throws System.NotSupportedException

ข้อผิดพลาดคือ:

LINQ to Entities ไม่รู้จักวิธีการ 'System.Collections.Generic.IQueryable`1[Thing] ActiveThing' และวิธีนี้ไม่สามารถแปลเป็นนิพจน์ของร้านค้าได้

คุณได้รับแนวคิด: ฉันต้องการวิธีแสดงออกอย่างคล่องแคล่วเพื่อที่ ISoftDeletable ใดๆ ฉันสามารถเพิ่มส่วนคำสั่ง 'where' ด้วยโค้ดที่เรียบง่ายและสามารถนำมาใช้ซ้ำได้ ตัวอย่างที่นี่ใช้ไม่ได้เนื่องจาก Linq2Entities ไม่รู้ว่าต้องทำอย่างไรกับเมธอด Active() ของฉัน

ตัวอย่างที่ฉันให้ไว้นี้เป็นตัวอย่างง่ายๆ แต่ในโค้ดจริงของฉัน ส่วนขยาย Active() มีชุดเงื่อนไขที่ซับซ้อนกว่านี้มาก และฉันไม่ต้องการคัดลอกและวางเงื่อนไขนั้นไปทั่วโค้ดของฉัน

มีข้อเสนอแนะอะไรบ้าง?


person Shaul Behr    schedule 29.08.2013    source แหล่งที่มา
comment
คุณไม่ได้บอกว่า อะไร มีข้อยกเว้นคือคุณได้รับ ซึ่งทำให้ยากขึ้นในการช่วยเหลือคุณ ฉันคาดหวังว่าสิ่งที่คุณทำมาจนถึงตอนนี้จะไม่เป็นไร แต่ตัวอย่างที่ซับซ้อนกว่านี้คงไม่เป็นเช่นนั้น เช่น ใช้ Active ในการเข้าร่วม   -  person Jon Skeet    schedule 29.08.2013
comment
@JonSkeet - เพิ่มข้อความแสดงข้อผิดพลาด แต่ใช่ คุณพูดถูก การใช้งานจริงของฉันเกี่ยวข้องกับการเข้าร่วม   -  person Shaul Behr    schedule 29.08.2013
comment
และคุณได้ทำซ้ำข้อผิดพลาดนั้นด้วยโค้ดง่ายๆ ของคุณหรือไม่? ฉันสงสัยว่าไม่ - มันจะมีประโยชน์ถ้ามีโค้ดตัวอย่างที่ ไม่ ทำให้เกิดข้อผิดพลาดอีกครั้ง   -  person Jon Skeet    schedule 29.08.2013
comment
@JonSkeet - แก้ไขเพื่อแสดงสิ่งที่ใกล้เคียงกับโค้ดจริงของฉันมากขึ้น แน่นอนว่าฉันไม่สามารถคอมไพล์เพื่อดูว่าสิ่งนี้ล้มเหลวจริง ๆ หรือไม่ แต่ก็ดีกว่าการใส่โค้ดจริง ๆ ให้กับคุณ!   -  person Shaul Behr    schedule 29.08.2013
comment
@JonSkeet, BTW คุณสังเกตเห็นไหมว่าฉันใช้แบบแผนเดียวกันกับข้อมูลโค้ดที่คุณใช้ใน C# ในระดับความลึก :-) ฉันชอบหนังสือเล่มนี้มาก - ขอบคุณ!   -  person Shaul Behr    schedule 29.08.2013
comment
ฉันไม่ได้ แต่ฉันดีใจที่มันกำลังติดตาม ตอนนี้คุณได้แสดงโค้ดแล้ว มันสมเหตุสมผลมากขึ้น เพราะตอนนี้คุณมีแผนผังนิพจน์ที่มีการเรียก Active ซึ่งไม่เหมือนกับการเรียก Active ด้วยตัวคุณเอง (เหมือนเมื่อก่อน)   -  person Jon Skeet    schedule 29.08.2013
comment
@ JonSkeet ยอมรับว่าตอนนี้มันสมเหตุสมผลมากขึ้น แต่คุณมีวิธีแก้ไขหรือไม่? :-)   -  person Shaul Behr    schedule 29.08.2013
comment
น่าเสียดายที่ไม่มีการเขียนแผนผังนิพจน์ใหม่ :(   -  person Jon Skeet    schedule 29.08.2013


คำตอบ (2)


คุณมีปัญหาสองประการที่ไม่เกี่ยวข้องในโค้ด ประการแรกคือ Entity Framework ไม่สามารถจัดการกับการร่ายในนิพจน์ได้ ซึ่งวิธีการขยายของคุณทำ

วิธีแก้ไขปัญหานี้คือการเพิ่มข้อจำกัด class ให้กับวิธีการขยายของคุณ หากคุณไม่เพิ่มข้อจำกัดดังกล่าว นิพจน์จะถูกคอมไพล์เพื่อรวมนักแสดง:

.Where (t => !((ISoftDeletable)t.IsDeleted))

การแสดงด้านบนทำให้ Entity Framework สับสน นั่นคือสาเหตุว่าทำไมคุณถึงได้รับข้อผิดพลาดรันไทม์

เมื่อมีการเพิ่มข้อจำกัด นิพจน์จะกลายเป็นการเข้าถึงคุณสมบัติแบบธรรมดา:

 .Where (t => !(t.IsDeleted))

นิพจน์นี้สามารถแยกวิเคราะห์ได้ดีกับกรอบงานเอนทิตี

ปัญหาที่สองคือคุณไม่สามารถใช้วิธีการขยายที่ผู้ใช้กำหนดในไวยากรณ์แบบสอบถามได้ แต่คุณสามารถใช้วิธีการดังกล่าวในไวยากรณ์ Fluent:

db.ThingContainers.SelectMany(tc => tc.Things).Active()
    .Any(t => t.SatisfiesOtherCondition); // this works

หากต้องการดูปัญหา เราต้องดูว่าแบบสอบถามที่สร้างขึ้นจริงจะเป็นอย่างไร:

 db.ThingContainers
       .Where(tc => tc.Things.Active().Any(t => t.StatisfiesOtherCondition))
       .Select(tc => new { ... });

การเรียก Active() จะไม่ถูกดำเนินการ แต่ถูกสร้างขึ้นเป็นนิพจน์สำหรับ EF ที่จะแยกวิเคราะห์ แน่นอนว่า EF ไม่รู้ว่าจะทำอย่างไรกับฟังก์ชันดังกล่าว ดังนั้นจึงปิดตัวลง

วิธีแก้ปัญหาที่ชัดเจน (แม้ว่าจะไม่สามารถทำได้เสมอไป) คือการเริ่มการสืบค้นที่ Things แทนที่จะเป็น ThingContainers:

db.Things.Active().SelectMany(t => t.Container);

วิธีแก้ปัญหาที่เป็นไปได้อีกประการหนึ่งคือการใช้ Model Defined Functions แต่นั่นเป็นกระบวนการที่เกี่ยวข้องมากกว่า ดูสิ่งนี้, สิ่งนี้ และ นี้ บทความ MSDN สำหรับข้อมูลเพิ่มเติม

person felipe    schedule 29.08.2013
comment
มันช่วยฉันได้ในประเด็นที่เกี่ยวข้อง ดังนั้น +1 สำหรับสิ่งนั้น ขอบคุณ! - person Shaul Behr; 29.08.2013
comment
@ Shaul ฉันได้ขยายคำตอบแล้ว โดยพื้นฐานแล้ว การผสมไวยากรณ์คิวรีกับวิธีการขยายถือเป็นเรื่องยุ่งยาก - person felipe; 30.08.2013
comment
ดีมาก แต่คุณจะทำอย่างไรถ้า ThingContainer เท่ากับ ISoftDeletable และฉันก็ต้องการใช้ .Active() ด้วย :-) - person Shaul Behr; 30.08.2013
comment
db.Things.Active().SelectMany(t => t.Container).Active() ;) แน่นอนว่าสามารถไปถึงจุดที่คุณต้องทำซ้ำนิพจน์ได้ หากแบบสอบถามซับซ้อนเพียงพอ - person felipe; 30.08.2013
comment
ดูคำตอบของฉันสำหรับสิ่งที่ฉันทำในตอนท้าย ขอบคุณสำหรับความช่วยเหลือของคุณ! - person Shaul Behr; 30.08.2013

ในขณะที่ @felipe ได้รับเครดิตคำตอบ ฉันคิดว่าฉันจะโพสต์คำตอบของตัวเองเป็นทางเลือก แม้ว่าจะคล้ายกันก็ตาม:

var query = from tc in db.ThingContainers.Active() // ThingContainer is also ISoftDeletable!
            join t in db.Things.Active() on tc.ID equals t.ThingContainerID into things
            where things.Any(t => t.SatisfiesOtherCondition)
            select new { ... };

สิ่งนี้มีข้อดีในการรักษาโครงสร้างของแบบสอบถามให้เหมือนเดิมไม่มากก็น้อย แม้ว่าคุณจะสูญเสียความคล่องของความสัมพันธ์โดยนัยระหว่าง ThingContainer และ Thing ก็ตาม ในกรณีของฉัน การแลกเปลี่ยนควรที่จะระบุความสัมพันธ์อย่างชัดเจน แทนที่จะต้องระบุเกณฑ์ Active() อย่างชัดเจน

person Shaul Behr    schedule 29.08.2013