.Net 7 Minimal API - using .Where causing error - c#

I am doing some learning on .Net 7 and minimal API's
I am simply trying to return a list of all timesheets within a payperiod. but I get the following error
Error CS1061 'Task<List>' does not contain a definition for
'Where' and no accessible extension method 'Where' accepting a first
argument of type 'Task<List>' could be found (are you
missing a using directive or an assembly reference?)
group.MapGet("/Payperiod/{payperiod}", async Task<Results<Ok<Timesheet>, NotFound>> (string payperiod, TimesheetAPIContext db) =>
{
return await db.Timesheet.ToListAsync().Where(t => t.Contains(payperiod))
is Timesheet model
? TypedResults.Ok(model)
: TypedResults.NotFound();
})
.WithName("GetTimesheetByPayPeriod")
.WithOpenApi();
Can someone help me understand what is going on here and how I should be writing this endpoint?
Cheers

First, you should put the where() before ToListAsync(), simply, you can think all linq's meothd end with Async is final method, you should not chain invoke after it.
Second, you can not use the Is Timesheet to check where it is empty. you should use the Any().
the modifed code as below:
group.MapGet("/Payperiod/{payperiod}", async Task<Results<Ok<Timesheet>, NotFound>> (string payperiod, TimesheetAPIContext db) =>
{
var ret = await db.Timesheet.Where(t => t.Contains(payperiod)).ToListAsync();
return ret.Any()
? TypedResults.Ok(ret)
: TypedResults.NotFound();
})
.WithName("GetTimesheetByPayPeriod")
.WithOpenApi();

You can't use .Where on a Task you need to first call .Result to actually get a List.
return await db.Timesheet.ToListAsync().Result.Where(t => t.Contains(payperiod))
is Timesheet model
? TypedResults.Ok(model)
: TypedResults.NotFound();

First of all, it looks like (looking at your responses in comments) you are writing asynchronous code without knowing how it works. Please correct me if I got that wrong.
The thing is, asynchronous programming can be very complex if you have no idea of what you are doing.
Have a look at these sources to understand asynchronous programming:
https://auth0.com/blog/introduction-to-async-programming-in-csharp/
https://www.codingame.com/playgrounds/4240/your-ultimate-async-await-tutorial-in-c/introduction
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model
Now let's take a look at your code:
group.MapGet("/Payperiod/{payperiod}", async Task<Results<Ok<Timesheet>, NotFound>> (string payperiod, TimesheetAPIContext db) =>
{
return await db.Timesheet.ToListAsync().Where(t => t.Contains(payperiod))
is Timesheet model
? TypedResults.Ok(model)
: TypedResults.NotFound();
})
.WithName("GetTimesheetByPayPeriod")
.WithOpenApi();
This line return await db.Timesheet.ToListAsync().Where(t => t.Contains(payperiod)) is where the problem exists.
You are trying to retrieve the Timesheet table from the database, then asynchronously convert it to a List<> and filter the result.
The problem is that you should wait for the ToListAsync() task to finish before you can filter the result with Where.
This is one example of how you can fix it (mind the extra parentheses):
group.MapGet("/Payperiod/{payperiod}", async Task<Results<Ok<Timesheet>, NotFound>> (string payperiod, TimesheetAPIContext db) =>
{
return (await db.Timesheet.ToListAsync()).Where(t => t.Contains(payperiod))
is Timesheet model
? TypedResults.Ok(model)
: TypedResults.NotFound();
})
.WithName("GetTimesheetByPayPeriod")
.WithOpenApi();

Related

Return Task<IReadOnlyCollection<T>> from EF Core query without await?

I like to be as specific as is possible with my types. When offering up a method encapsulating a query it's common to see List<T> returned (or more specifically Task<List<T>>.) No wonder with EF Core's .ToListAsync() method. But that's not very accurate is it? List<T> is a projection of a larger collection in SQL Server (or whatever persistence you're using). It's not modifiable.
I want to instead return IReadOnlyCollection<T> from my methods. This seems a lot more accurate. I'm returning a projection from my source data and you can't modify the source data by modifying the projection. Nor can my result be modified erroneously without casting back to List<T> at which point you've gone far enough out of your way I'm assuming you know what you're doing.
So I attempted to just return the interface like I would synchronously.
public Task<IReadOnlyCollection<TResult>> GetResultAsync() =>
_dbContext.Where(x => x.Property == true).ToListAsync();
I'm avoiding using the async and await operators here. I want to pass the Task unawaited to avoid the await overhead. But when wrapped in Task<T> C#'s type system doesn't recognize that the wrapped List<T> is castable to IReadOnlyCollection<T>. So Task<List<T>> cannot be cast to Task<IReadOnlyCollection<T>>. You can try an explicit cast, of course, but the compiler won't like that either and for the same reasons.
Well that's annoying. But we can peel off the Task<T> with async wait easy enough.
public async Task<IReadOnlyCollection<TResult>> GetResultAsync() =>
await _dbContext.Where(x => x.Property = true).ToListAsync();
In order to avoid the added code I considered an extension method.
public static async Task<IReadOnlyCollection<T>> ToReadOnlyCollectionAsync<T>(this IQueryable<T> query) =>
await query.ToListAsync();
This gets the await out of my methods but of course still adds another await. Is there a way to return Task<IReadOnlyCollection<T>> for "free" from EF Core 2.1?
No idea what ToListAsync() does but going by the name, and if I'm guessing correctly, it iterates the results and converts them to a list on a background thread and returns the task that must be awaited before the list is produced.
If that's the case, your question is
how to convert Task<List<T>> to Task<IReadonlyCollection<T>>.
If so, you could in theory do something like this:
public Task<IReadOnlyCollection<string>> ToReadOnlyCollection()
{
return ToListAsync().ContinueWith(x => new ReadOnlyCollection<string>(x.Result) as IReadOnlyCollection<string>);
}
public Task<IReadOnlyList<string>> ToReadOnlyList()
{
// if you don't mind an IReadOnlyList, you can use this
// one which doesn't involve creating a new collection
return ToListAsync().ContinueWith(x => x.Result as IReadOnlyList<string>);
}
private Task<List<string>> ToListAsync()
{
return Task.Run(() =>
{
Task.Delay(1000);
return new List<string>
{
"1",
"2",
"3"
};
});
}
I don't believe Task<T> allows contra-variance so a Task<TChild> (where Child : Parent) cannot be automatically converted to Task<TParent> so you'll need to do it yourself.

EfCore ToListAsync() Throw exception with where condition

I have a problem with an async task that get items from the database with a specific condition
When i call the ToListAsync() i get this error
Expression of type 'System.Collections.Generic.IAsyncEnumerable1[System.Guid]' cannot be used for constructor parameter of type 'System.Collections.Generic.IEnumerable1[System.Guid]'
Parameter name: arguments[0]
This is the code snippet:
public async Task<IEnumerable<Parent>> GetItems(List<Guid> TypeIds)
{
var items = context.Parent.Include(x => x.Child).Where(x => new HashSet<Guid>(x.Child.Select(y => y.TypeId).Distinct()).SetEquals(new HashSet<Guid>(TypeIds)));
return await items.Include(x=> x.Child).ToListAsync();
}
If I implement this method not async I won't get the error and all works.
You can't have a Where lambda like that for Entity Framework, remember that the expression is going to get converted to SQL which has no idea what a HashSet is. You are probably looking for something like this instead:
var items = context.Parent
.Include(x => x.Child)
.Where(x => x.Child.Any(y => TypeIds.Contains(y.TypeId)));

Using EF Include on TPH

I have implemented code first db architecture with simple inheritance using THP:
And I need to query all notifications of all type.
TargetUser property in NotificationUser table is a association.
I'm trying to execute next code:
var notifications = _context.Notifications;
foreach (var notification in notifications)
{
Debug.WriteLine((notification is NotificationUser)? ((NotificationUser) notification).TargetUser?.Name : "-");
}
In database propety TargetUser is set to correct foreign key, but in code I don't get any result. Lazy loading is enabled.
Is it possible to user eager loading? I had already tried to write _context.Notifications.Include('TargetUser') byt it throws an exception.
Upd. The exception is:
A specified Include path is not valid. The EntityType 'Core.Concrete.NotificationBase' does not declare a navigation property with the name 'TargetUser'.
Tried to modify this answer to:
var notifications = _context.Notifications.OfType<NotificationUser>()
.Include(n => n.TargetUser)
.Cast<NotificationBase>()
.Union(_context.Notifications.OfType<NotificationPlace>()
but still the same exception is thrown.
I know this is an old thread, but I'd still like to post some improvements for somebody looking for the same solution.
1. Network Redundancy
Selecting Ids and then running a query, that loads items with the Ids is redundant and the same effect can be achieved by simply running this
Solution:
var userNotifications = _context.Notifications
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit)
.OfType<NotificationUser>()
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.ToList();
That way, you aren't waiting for 2 DB connections, but just one. Also you save some traffic.
2. Paging on ignored entities?
One would assume, that this specific method is used for only viewing Entities of an inherited type so I would expect Skip and Take to work directly on only entities of said type. e.g. I want to skip 10 NotificationUsers, not 10 Users (of which only 4 are NotificationUsers for example).
Solution: Move ofType higher up the query
var userNotifications = _context.Notifications
.OfType<NotificationUser>()
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit)
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.ToList();
3. Async/Await
When writing an API, you should think about using async/await as that doesn't block the thread thus wastes less resources (this will probably require you to rewrite a lot of your existing code if you don't use it already though).
Please study the advantages of async/await and use them in scenarios like waiting for a result.
Solution: Change this
private List<NotificationUser> GetNotificationUsers(int offset, int limit)
{
return _context.Notifications
.OfType<NotificationUser>()
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit)
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.ToList();
}
into this
private async Task<List<NotificationUser>> GetNotificationUsersAsync(int offset, int limit)
{
return await _context.Notifications
.OfType<NotificationUser>()
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit)
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.ToListAsync();
}
NOTE:
You also then have to change any place that uses this method from
var x = GetNotificationUsers(skip, take);
to
var x = await GetNotificationUsersAsync(skip, take);
And make that method async and return a task as well
I don't know about the amount of entities you will work with. If possible, I would try to do the union not on the DB server:
var userNotifications = _context.Notifications.OfType<NotificationUser>()
.Include(n => n.TargetUser).ToList();
var placeNotifications = _context.Notifications.OfType<NotificationPlace>().ToList();
var notifications = userNotifications.Union(placeNotifications);
See https://stackoverflow.com/a/27643393/2342504
Already tried a lot of different solutions, and none fit my requirements, as I'm working on an API, and the query must support pagination and make constant requests to the database (and not loading all entities in memory).
Finally found out a solution, perhaps not the best, but enough for now.
Firstly, I request a portion of ordered data (pagination logic):
var notifications = _context.Notifications
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit);
(At this point I'm not interested in any properties) Next, I'm getting the Id's of loaded items for each entity type:
var ids = notifications.OfType<NotificationUser>().Select(n => n.Id).ToList();
and lastly load specific entities including all properties:
var userNotifications = _context.Notifications.OfType<NotificationUser>()
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.Where(n => ids.Contains(n.Id))
.ToList();
all entities goes to list and sorted one more time.
A lot of bad stuff here, hope someone can provide better solution.
If I understood well, you want to get all the entities from the table + the property TargetUser when relevant (for entities of type NotificationUser). You said that "Lazy loading is enabled", which is a good thing.
You tried something like (updated to latest version of C#):
var notifications = _context.Notifications;
foreach (var notification in notifications)
{
Debug.WriteLine((notification is NotificationUser notificationUser)
? notificationUser.TargetUser?.Name
: "-");
}
If you don't get any result, it probably means that your entities are badly configured to work with lazy loading:
Are your classes public?
Are your navigation properties defined as public, virtual?
See: https://learn.microsoft.com/en-us/ef/ef6/querying/related-data#lazy-loading

Getting a single object from mongodb in C#

I've picked up a piece of code that is using the MongoDB driver like this to get a single object from a collection...this can't be right, can it? Is there a better way of getting this?
IMongoCollection<ApplicationUser> userCollection;
....
userCollection.FindAsync(x => x.Id == inputId).Result.ToListAsync().Result.Single();
Yes, there is.
First of all don't use FindAsync, use Find instead. On the IFindFluent result use the SingleAsync extension method and await the returned task inside an async method:
async Task MainAsync()
{
IMongoCollection<ApplicationUser> userCollection = ...;
var applicationUser = await userCollection.Find(_ => _.Id == inputId).SingleAsync();
}
The new driver uses async-await exclusively. Don't block on it by using Task.Result.
You should limit your query before executing, otherwise you will first find all results and then only read one of it.
You could either specify the limit using FindOptions in FindAsync, or use the fluent syntax to limit the query before executing it:
var results = await userCollection.Find(x => x.Id == inputId).Limit(1).ToListAsync();
ApplicationUser singleResult = results.FirstOrDefault();
The result from ToListAsync will be a list but since you limited the number of results to 1, that list will only have a single result which you can access using Linq.
In newer versions of MongoDB Find() is deprecated, so you can either use
collection.FindSync(o => o.Id == myId).Single()
or
collection.FindAsync(o => o.Id == myId).Result.Single()
You can also use SingleOrDefault(), which returns null if no match was found instead of throwing an exception.
I could not get the method:
coll.Find(_ => _.Id == inputId).SingleAsync();
To work as I was getting the error
InvalidOperationException: Sequence contains more than one element c#
So I ended up using .FirstOrDefault()
Example:
public FooClass GetFirstFooDocument(string templatename)
{
var coll = db.GetCollection<FooClass>("foo");
FooClass foo = coll.Find(_ => _.TemplateName == templatename).FirstOrDefault();
return foo;
}

Include a collection and then a collection one level down

I am trying to query a collection and child collection using EF 7. Here's the code:
public async Task < List < Table >> GetAllTable() {
var tableList = await db.Tables.Include(o => o.Checks.Select(i => i.CheckItems)).ToListAsync();
return tableList;
}
I'm following the syntax from here MSDN. However, when i run this code i'm getting the following error. Does anyone know what went wrong here? Thanks!
InvalidCastException: Unable to cast object of type
'Remotion.Linq.Clauses.Expressions.SubQueryExpression' to type
'System.Linq.Expressions.MemberExpression'.**
Microsoft.Data.Entity.Query.EntityQueryModelVisitor.b__30_2(<>f__AnonymousType2`2
<>h__TransparentIdentifier0)
The documentation you are reading is for EF 5.
The design meeting notes for EF 7 say the syntax of this has changed - try this:
db.Tables.Include(t => t.Checks)
.ThenInclude(c => c.CheckItems)
.ToListAsync()

Categories

Resources