C#带有多个联接的Linq查询,带有一个await

6
无论我在查询语句中添加 await 的位置在哪里,智能感知都会报错。只需要添加一个 await 即可。这是使用 EntityFramework Core 的 .NET Core 2.x。
 public async Task<IEnumerable<BalanceItemResource>> GetBalanceItems(int fyId)
 {
 IEnumerable<BalanceItemResource> lQuery = (IEnumerable<BalanceItemResource>)
            from r1 in _context.Requests
            join u1 in _context.Users
            on r1.ApproverId equals u1.Id
            join p1 in _context.Purchases
            on r1.PurchaseId equals p1.Id
            join o1 in _context.OfficeSymbols
            on u1.Office equals o1.Name
            where r1.FYId == fyId
            select new { r1.Id, 
                p1.PurchaseDate, 
                officeId = o1.Id, 
                officeName = o1.Name, 
                o1.ParentId, o1.Level, 
                total = r1.SubTotal + r1.Shipping

            };
            return lQuery;

    }
2个回答

12

C# Linq 代码只能 await 那些将查询实例化并加载它的操作,例如 ToListAsyncToDictionaryAsync。这些方法位于命名空间 System.Data.Entity 而不是 System.Linq

public async Task<List<BalanceItemResource>> GetBalanceItems(int fyId)
{
    var query = // Ensure `query` is `IQueryable<T>` instead of using `IEnumerable<T>`. But this code has to use `var` because its type-argument is an anonymous-type.
        from r1 in _context.Requests
        join u1 in _context.Users
        on r1.ApproverId equals u1.Id
        join p1 in _context.Purchases
        on r1.PurchaseId equals p1.Id
        join o1 in _context.OfficeSymbols
        on u1.Office equals o1.Name
        where r1.FYId == fyId
        select new { r1.Id, 
            p1.PurchaseDate, 
            officeId = o1.Id, 
            officeName = o1.Name, 
            o1.ParentId,
            o1.Level, 
            total = r1.SubTotal + r1.Shipp
        };

    var list = await query.ToListAsync().ConfigureAwait(false); // <-- notice the `await` here. And always use `ConfigureAwait`.
    
    // Convert the anonymous-type values to `BalanceItemResource` values:
    return list
        .Select( r => new BalanceItemResource() {
            PurchaseDate = p1.PurchaseDate, 
            officeId = o1.Id, 
            officeName = o1.Name, 
            ParentId = o1.ParentId,
            Level = o1.Level, 
            total = r1.SubTotal + r1.Shipp
        } )
        .ToList();
}

话虽如此,看起来你正在使用Entity Framework - 假设你已经设置了外键导航属性,你可以将查询简化为以下内容:

public async Task<List<BalanceItemResource>> GetBalanceItems(int fyId)
{
    var query = _context.Requests
        .Include( r => r.ApproverUser )       // FK: Users ApproverId        // <-- These `.Include` lines aren't necessary when querying using a projection to an anonymous type, but I'm using them for illustrative purposes.
        .Include( r => r.Purchase )           // FK: Purchases PurchaseId
        .Include( r => r.AproverUser.Office ) // FK: OfficeSymbols Name
        .Select( r => new
        {
            r.Purchase.PurchaseDate,
            officeId   = r.AproverUser.Office.Id,
            officeName = r.AproverUser.Office.Name,
            r.AproverUser.Office.ParentId,
            r.AproverUser.Office.Level,
            total      = r.SubTotal + r.Shipp
        } );

    var list = await query.ToListAsync().ConfigureAwait(false);

    return list
        .Select( r => new BalanceItemResource() {
            PurchaseDate = p1.PurchaseDate, 
            officeId = o1.Id, 
            officeName = o1.Name, 
            ParentId = o1.ParentId,
            Level = o1.Level, 
            total = r1.SubTotal + r1.Shipp
        } )
        .ToList();
}

或者一行代码:

public async Task<List<BalanceItemResource>> GetBalanceItems(int fyId)
{
    return
        (
            await _context.Requests
                .Select( r => new
                {
                    r.Purchase.PurchaseDate,
                    officeId   = r.AproverUser.Office.Id,
                    officeName = r.AproverUser.Office.Name,
                    r.AproverUser.Office.ParentId,
                    r.AproverUser.Office.Level,
                    total      = r.SubTotal + r.Shipp
                } )
                .ToListAsync()
                .ConfigureAwait(false)
        )
        .Select( r => new BalanceItemResource() {
            PurchaseDate = p1.PurchaseDate, 
            officeId = o1.Id, 
            officeName = o1.Name, 
            ParentId = o1.ParentId,
            Level = o1.Level, 
            total = r1.SubTotal + r1.Shipp
        } )
        .ToList();
}

在这种情况下,我们无法省略await(并直接返回Task),因为将其转换为BalanceItemResource是在内存中的Linq(Linq-to-Objects)中进行的,而不是在Linq-to-Entities中进行的。

@Ramious,我犯了一个错误,我会尽快更新我的答案。 - Dai
1
@Dai 为什么要使用 ConfigureAwait?如果不是至关重要,那么默认情况下它的效果很好。此外,在需要使用相同上下文的情况下,它会导致错误(例如 UI 中的跨线程访问)。你不知道上下文,所以我不会走得太远 :) - Michał Turczyn
1
你是对的。我只是在写一般性的注释,就像你的评论“始终使用ConfigureAwait”一样,这可能意味着你像我用的方式一样使用它 :) - Michał Turczyn
我认为ConfigureAwait很危险,因为我不知道它会产生什么影响。除此之外,我完全同意你的看法 :) - Michał Turczyn
@TheodorZoulias 正确。我使用 ConfigureAwait(true) 作为有意 使用 ConfigureAwait(false) 的指示。我建议其他人也这样做。我不知道为什么我对这个观点会受到如此多的抨击 D: - Dai
显示剩余5条评论

1

当你请求另一个进程为你执行某些操作时,几乎总是使用异步等待:请求读取文件、执行数据库查询或从互联网获取一些信息。

通常,在这个任务正在执行的过程中,你无法做任何事情来加速它。你只能等待这个进程完成它的任务,或者四处看看是否可以在此期间做其他事情。

如果你仔细观察IQueryable LINQ语句,你会发现有两组:返回IQueryable<TResult>和不返回的函数。第一组是Where、GroupBy、Select等函数。后一组包含ToList()、ToDictionary()、Count()、Any()、FirstOrDefault()等函数。

第一组函数(返回IQueryable)只是改变必须执行的查询的表达式。查询本身没有被执行,数据库也没有被联系。这些函数使用延迟执行。

只有在你使用后一组函数之一,或者开始枚举自己,使用foreach,甚至在更低的级别上:GetEnumerator()和MoveNext(),表达式才会被转换并发送到必须执行查询的进程。

当另一个进程正在执行查询时,你可以无所事事地等待该进程完成,或者可以使用async await来做其他事情,比如保持UI的响应性(典型的例子),但当然你也可以通过命令另一个进程为你做点事情来加速处理。因此,在查询数据库的同时,你可以读取文件或从互联网获取一些信息。
因为IQueryable函数不执行查询,只更改查询中的表达式,所以你不会看到像WhereAsync、SelectAsync等函数。
你会发现像ToListAsync()、AnyAsync()、FirstOrDefaultAsync()这样的函数。
但这与我的问题有什么关系呢?
你应该决定:我的GetBalanceItems函数应该返回什么?它应该返回平衡项目吗?还是应该返回查询平衡项目的可能性。差别微妙,但非常重要。
如果你返回平衡项目,那么你已经执行了查询。如果你返回查询平衡项目的可能性,你就返回了一个IQueryable:类似于LINQ函数Where()和Select(),查询没有被执行。
你应该返回什么取决于你的调用者想要做什么:他们想要所有获取的数据,还是可能希望将数据与其他LING函数连接起来。例如:
// Get the OfficeId and OfficeName of all balance items with a zero total:
var balanceItemsWithZeroTotal = GetBalanceItems()

    // keep only those balnceItems with zero total
    .Where(balanceItem => balanceItem.Total == 0)

    // from the remaining balanceItems select the OfficeId and OfficeName
    .Select(balanceItem => new
    {
         OfficeId = balanceItem.OfficeId,
         OfficeName = balanceItem.OfficeName,
    });

如果您的呼叫者想要执行类似此操作的事情,那么如果您执行查询并获取所有BalanceItems,然后您的呼叫者会丢弃大部分已获取的数据,这将是一种浪费。

在使用LINQ时,最好尽可能保持查询IQueryable。让您的呼叫者执行查询(ToList()、Any()、FirstOrDefault()等)

因此,在您的情况下:不要调用枚举函数(如ToList()),也不要使您的函数异步并返回IQueryable:
public IQueryable<BalanceItemResource>> QueryBalanceItems(int fyId)
{  
    return from r1 in _context.Requests
        join u1 in _context.Users
        on r1.ApproverId equals u1.Id
        ...
        select new BalanceItemResource() {...};
}

这个规则有一个例外!

您的函数使用了_context。在查询未执行时,必须保持此上下文的活动状态:您的调用者必须确保在执行查询之前不会Dispose上下文。通常情况下,可以按照以下方式完成此操作:

class MyRepository : IDisposable
{
    private readonly MyDbContext dbContext = ...

    // Standard Dispose pattern
    public void Dispose()
    {
        Dispose(true)
    }

    protected virtual void Dispose(bool disposing)
    {
         if (disposing)
         {
             this.dbContext.Dispose();
         }
    }

    // your function:
    public IQueryable<BalanceItemResource>> QueryBalanceItems(int fyId) {...}

    // other queries
    ...
 }

使用方法:

using (var repository =  new MyRepository(...))
{
    // the query described above:
    var queryBalanceItemsWithZeroTotal = GetBalanceItems()
    .Where(balanceItem => balanceItem.Total == 0)
    .Select(balanceItem => new
    {
         OfficeId = balanceItem.OfficeId,
         OfficeName = balanceItem.OfficeName,
    });

    // note: the query is still not executed! Execute it now async-await:
    var balancesWithZeroTotal = await queryBalanceItemsWithZeroTotal.ToListAsync();

    // or if you want: start executing and do something else instead of waiting idly:
    var queryTask = queryBalanceItemsWithZeroTotal.ToListAsync();

    // because you didn't await, you are free to do other things 
    // while the DBMS executes your query:
    DoSomethingElse();

    // now you need the result of the query: await for it:
    var var balancesWithZeroTotal = await queryTask;
    Process(balancesWithZeroTotal);
}

所以一定要在 using 语句结束之前执行查询。如果您不这样做,将会出现运行时错误,提示您的上下文已被处理。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接