简体   繁体   中英

C# Linq Query with Multiple Joins with an await

No matter where I try to add an await in my query I get an intellisence error. Just need to get an await inserted. This is dotnet core 2.x with EntityFramework core

 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;

    }

C# Linq code can only await operations that materialize the query and load it, such as ToListAsync and ToDictionaryAsync . These methods are in namespace System.Data.Entity and not 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();
}

That said, it looks like you're using Entity Framework - assuming you have foreign-key navigation properties set-up you can simplify your query to this:

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();
}

Or a one-liner:

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();
}

In this particular case we can't elide the await (and simply return the Task directly) because the conversion to BalanceItemResource happens in in-memory Linq (Linq-to-Objects), not Linq-to-Entities.

Async-await is almost always used when you ask another process to do something for you: ask for a file to be read, a database query to be executed, or some information to be fetched from the internet.

Normally while this other task is executing your order, you can't do anything to speed this up. All you can do is wait for this other process to finish its task, or look around to see if you can do something else in the meantime.

If you look closely to IQueryable LINQ statements, you'll find there are two groups: the ones that return IQueryable<TResult> and the ones that don't. The first group are functions like Where, GroupBy, Select, etc. The latter group contains functions like ToList(), ToDictionary(), Count(), Any(), FirstOrDefault().

The functions of the first group (return IQueryable) do nothing but change the Expression of the query that must be executed. The query itself is not executed, the database is not contacted. It is said that these function use deferred execution.

Only after you use one of the functions of the latter group, or start enumerating yourself, using foreach, or even at a lower level: GetEnumerator() and MoveNext(), the Expression is translated and sent to the process that must execute the query.

While the other process is executing the query, you could wait idly for this process to finish, or you can use async await to do other things instead, like keeping the UI responsive (the typical example), but of course you could also speed up processing by ordering another process to do something for you. So while querying the database, you could read a file, or get some information from the internet.

So because the IQueryable functions don't execute the query, only change the Expression in the query, you won't see functions like WhereAsync, SelectAsync etc.

You will find functions like ToListAsync(), AnyAsync(), FirstOrDefaultAsync().

But what does this have to do with my question?

You should decide: what should my function GetBalanceItems return? Should it return the balanced items? Or should it return the possibility to query the balanced items. The difference is subtle, but very important.

If you return the balanced items, you already have executed the query. If you return the possibility to query the balanced items, you return an IQueryable: similar to LINQ functions as Where() and Select(), the query is not executed.

What you should return depends on what your callers want to do: do they want all fetched data, or could it be that they want to concatenate the data with other LING functions. For example:

// 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,
    });

If your callers want to do things like this, it would be a waste if you would execute the query and fetch all BalanceItems after which your caller would throw away most fetched data.

When working with LINQ it is best to keep your query IQueryable as long as possible. Let your callers execute the query (ToList(), Any(), FirstOrDefault(), etc)

So in your case: don't call an enumerating function like ToList(), don't make your function async and return 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() {...};
}

One exception to this rule!

Your function uses _context . While your query is not executed, this context must be kept alive: your caller must take care that he does not Dispose the context before he executes the query. Quite often this is done as follows:

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
    ...
 }

Usage:

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);
}

So be sure to execute the query before the end of the using statement. If you don't do that you'll get a run time error saying that your context has been disposed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM