Possible to build query without DbContext instance? - c#

I am using Entity Framework 5. I would like to build a query (DbQuery?) then execute it on a DbContext. Is it possible?
Normally, I would perform a query like this:
using (var db = new MyDbContext())
{
var nike = db.Products.Where(p => p.Brand == "Nike").OrderBy(p => p.Name);
foreach (var product in nike)
{
Debug.WriteLine(product.Name);
}
}
But can I construct the query before creating the DbContext, and then attach the query to a DbContext instance when I actually want to retrieve the data ?

public IEnumerable<Products> GetProduct(MyDbContext db){
//query created before-hand
var nike = db.Products.Where(p => p.Brand == "Nike").OrderBy(p => p.Name);
return nike;
}
//and then in your method:
using (var db = new MyDbContext()){
var nike = GetProduct(db); //MyDbContext object attached here.
foreach(var product in nike){
Debug.WriteLine(product.Name);
}
}
Maybe this is what you wanted to do? I am not sure.

A solution would be following:
public Func<bool,Products> Filter()
{
return i => i.Brand == "Nike"
}
public Func<bool,Products> Filter(string brandName)
{
return i => i.Brand == brandName;
}
//usage:
db.Products.Where(Filter());
//or
db.Products.Where(Filter("Nike"));
Currently I cannot test, but it might be that you have to use Expression<Func<bool,Brand>>.
Can someone confirm that?

Related

EF Core - Modify realations without loading entity

I was trying to implement a function that will let a user like a comment. If the user has already liked it, it can't be liked again and vice versa.
This is what it looks like:
public async Task<ActionResult<CommentResponse>> LikeComment(LikeComment like)
{
if (like.HasNullProperty())
return BadRequest("Missing properties!");
var comment = await commentService.GetCommentWithLikes((int) like.CommentId);
if(comment is null)
return NotFound($"No comment with id {like.CommentId} was found");
try
{
var userId = User.GetUserID();
comment = await commentService.LikeComment(comment, userId, (bool)like.Liked);
return comment is not null ? Ok(comment.GetCommentResponse((bool)like.Liked)) : StatusCode(304);
}
catch(Exception e)
{
return StatusCode(500, $"Error while trying to {((bool)like.Liked ? "like" : "dislike")} comment");
}
}
Relevant functions:
public async Task<Comment> GetCommentWithLikes(int id) => await blogContext.Comments.IncludeLikes().FirstOrDefaultAsync(x => x.Id == id);
public static IQueryable<Comment> IncludeLikes(this IQueryable<Comment> source)
=> source.Select(x => new Comment
{
Id = x.Id,
ArticleId = x.ArticleId,
CreatedById = x.CreatedById,
CreatedAt = x.CreatedAt,
Likes = x.LikedBy.Count,
Text = x.Text,
});
And the main like logic:
public async Task<Comment> LikeComment(Comment comment, string userId, bool liked)
{
var user = new User { Id = userId };
var hasLiked = await blogContext.Comments.Where(x => x.Id == comment.Id && x.LikedBy.Any(x => x.Id == user.Id)).FirstOrDefaultAsync() is not null;
Action action = null;
if (!hasLiked && liked)
{
action = () => comment.LikedBy.Add(user);
comment.LikedBy = new List<User>();
comment.Likes++;
}
else if (hasLiked && !liked)
{
action = () => comment.LikedBy.Remove(user);
comment.LikedBy = new List<User> { user };
comment.Likes--;
}
if (action is null)
return null;
blogContext.Attach(user);
blogContext.Attach(comment);
action();
await blogContext.SaveChangesAsync();
return comment;
}
The idea was to not load the whole likedBy relation, but still notify EF Core that i have added or removed one user. Therefore i modify the Comment, then attach it so EF Core tracks the changes to the likedBy relation. Interestingly, it works fine when liking a comment. However, when disliking, i get an rrror that the comment is already attached. Using .AsNoTracking() in the GetCommentsWithLikes function didn't help.
The instance of entity type 'Comment' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
This is the comment passed to the like func when linking (works):
This is the one when disliking (only diff is the like count...):
And this is it right before the failing attach:
Maybe someone knows the reason for this behaviour and can help me or suggest a different approach :)
Thanks
Using .AsNoTracking() in the GetCommentsWithLikes function didn't help
Due to the used projection, that function is already implicitly no tracking. It is the following call
var hasLiked = await blogContext.Comments
.Where(x => x.Id == comment.Id && x.LikedBy.Any(x => x.Id == user.Id))
.FirstOrDefaultAsync() is not null;
which is adding a Comment instance to the change tracker when the result is not null.
Since you don't need that instance and are just checking for existence, use the following instead which doesn't involve entity instances, but pure server side query:
var hasLiked = await blogContext.Comments
.AnyAsync(x => x.Id == comment.Id && x.LikedBy.Any(x => x.Id == user.Id));

Using for loop on where() method

So I have a search-input and checkboxes that passes the values to the controller when there are inputs. And I want to use these values to get something back from the database. The search-input is a string and it works and intended. Here is the code for the search-input:
public async Task<ViewResult> Index(string searchString, List<int> checkedTypes)
{
var products = from p in _db.Products select p;
ViewData["CurrentFilter"] = searchString;
if (!string.IsNullOrEmpty(searchString))
{
products = products.Where(p => p.Name.ToLower().Contains(searchString));
}
return View(products);
}
However the checkboxes values are stored in a list. So basically I want to do the same as the code above, but with a list. So basically an idea is like this:
if(checkedTypes != null)
{
foreach (var i in checkedTypes)
{
products = products.Where(p => p.TypeId == i));
}
}
If I do it like the code above, I just get the last (i) from the loop. Another solution I did was this:
if(checkedTypes != null)
{
var temp = new List<Product>();
foreach (var i in checkedTypes)
{
temp.AddRange(products.Where(p => p.TypeId == i));
}
products = temp.AsQueryable();
}
But when I did it like that I get this error:
InvalidOperationException: The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IAsyncQueryProvider can be used for Entity Framework asynchronous operations.
So anyone have a solution that I can use? Or is there a better way to handle checkboxes in the controller?
Assuming you are using EF Core (also the same is true for linq2db) - it supports translating filtering with local collection, i.e. Where(x => checkedTypes.Contains(x.SomeId)).
If you have "and" logic to filter by searchString and checkedTypes than you can conditionally add Where clause:
if (!string.IsNullOrEmpty(searchString))
{
products = products.Where(p => p.Name.ToLower().Contains(searchString));
}
if(checkedTypes != null)
{
products = products.Where(p => checkedTypes.Contains(p.TypeId));
}
P.S.
Also you should be able to change your first line to:
var products = _db.Products.AsQueryable();

Linq code doesn't return correct record

I have a table named dbo.EmployeeType with three records:
PK_EmployeetypeID EmployeeTypeName
1 Project Manager
2 Business Analyst
3 Developer
I have this piece of Linq code:
public static string GetTypeByID(int id)
{
using (ProjectTrackingEntities1 db = new ProjectTrackingEntities1())
{
var type = db.EmployeeTypes.Select(o => new LOOKUPEmployeeType
{
PK_EmployeeTypeID = id,
EmployeeTypeName = o.EmployeeTypeName
});
return type.FirstOrDefault().EmployeeTypeName;
}
}
No matter what id I send to it, it returns Project Manager, and I'm confused as to why.
You need to apply a filter, otherwise you're just returning the first record and hard coding the ID. Try this:
public static string GetTypeByID(int id)
{
using (ProjectTrackingEntities1 db = new ProjectTrackingEntities1())
{
//Here we apply a filter, the lambda here is what creates the WHERE clause
var type = db.EmployeeTypes
.FirstOrDefault(et => et.PK_EmployeeTypeID == id);
if(type != null)
{
return type.EmployeeTypeName;
}
else
{
return "";
}
}
}
Note that using FirstOrDefault means if there are no matches, or multiple matches, type will be null and you will get an empty string returned.
Set a breakpoint on type = ... and inspect it. You have no Where in there so you get all - and Select just makes LOOKUPEmployeeTypes out of all of them.
FirstOrDefault then returns the first of those 3 which is always the ProjManager
Fix:
var type = db
.EmployeeTypes
.Where( o => o.Id == id)
.Select(o => new LOOKUPEmployeeType
{
PK_EmployeeTypeID = id,
EmployeeTypeName = o.EmployeeTypeName
});
In your code you only return the first value. You need to tell EF which value you need to return.
Let us assume you need the value with Id=2. Instead of Select(), use Single(x => x.Id == 2) or First(x => x.Id == 2).

EF 5.0 Simple Delete Statement

I'm trying to run a delete statement on EF 5.0. that deletes let say 5000 ~ 40000 records
here is my code:
using(myEntity ctx = new myEntity) {
var q = from s in ctx.MyTable.where( x => x.accountID == 1234) select s;
ctx.myTable.Remove(q);
// because there is no ctx.DeleteObject(whatever)
// since this is not EF 4.0
}
and here is the error:
cannot convert from 'System.Linq.IQueryable' to 'namespace.myTable'
any idea?
In your example, you are getting an IQueryable< myTable > and trying to pass it into a Remove method with the following signature:
public TEntity Remove(TEntity entity)
Since it only accepts an instance of an entity, you must actually convert the IQueryable to a list of entities and then iterate through them. I would most likely do it as follows myself:
using(var ctx = new myEntity())
{
ctx.myTable
.Where(x => x.accountId == 1234)
.ToList()
.ForEach(item => ctx.myTable.Remove(item));
ctx.SaveChanges();
}
You are trying to remove a LINQ query from your table (which makes no sense), as opposed to removing a table entry.
What you need to do is either this:
using(myEntity ctx = new myEntity) {
var q = from s in ctx.MyTable.where( x => x.accountID == 1234) select s;
foreach(var entry in q)
{
ctx.myTable.Remove(entry);
}
}
Or write a stored procedure, import it into Entity Framework and execute it.

Clean method in order to update collection in entity framework

i'm sorry if this question has already been asked, but i'm in trouble with my method of updating collection in Entity Framework.
Let me explain the situation :
- I have for example one model CUSTOMER with some properties and a collection of ORDERS (for example).
- Let's imagine we have an admin page on wich we can edit all the ORDERS for a customer, and when we submit the form, it will send us back the object CUSTOMERS with updated ORDERS (some added, some updated and some deleted).
For the moment i use something like this in order to compare old collection and new collection and determine which object i need to delete/update/add
var toRemove = new List<ORDERS>();
var toAdd = new List<ORDERS>();
foreach (
var order in
oldList.Where(
order =>
newList.FirstOrDefault(t => t.link_id == order.link_id) == null))
{
toRemove.Add(order);
}
foreach (
var order in
newList.Where(
order =>
oldList.FirstOrDefault(t => t.link_id == order.link_id) == null))
{
toAdd.Add(order);
}
foreach (var ORDERSe in toRemove)
{
bdd.ORDERS.Remove(ORDERSe);
}
foreach (var ORDERSe in toAdd)
{
ORDERSe.pjt_id = project_id;
bdd.ORDERS.Add(ORDERSe);
}
foreach (
var order in
newList.Where(
order =>
oldList.FirstOrDefault(t => t.link_id == order.link_id) != null))
{
var child = oldList.FirstOrDefault(t => t.link_id == order.link_id);
bdd.Entry(child).CurrentValues.SetValues(order);
}
But i'm unconfortable with this, because in my mind, entity framework should be able to do the work for me !
I was hoping something like :
customer.orders = newOrders;
Did i missed anything about entity framework or ?
Because when i do this, it just duplicate my orders.
Thanks in advance for your answer.
You can certainly make it cleaner using .Except() and .Intersect(), but the concept doesn't really change, AFAIK you still have to individually remove, update & add the entries in loops...
var oldList = new List<ORDERS>();
var newList= new List<ORDERS>();
var IdsToRemove = oldList.Select(t => t.link_id).Except(newList.Select(t => t.link_id));
var IdsToAdd = newList.Select(t => t.link_id).Except(oldList.Select(t => t.link_id));
var IdsToUpdate = newList.Select(t => t.link_id).Intersect(oldList.Select(t => t.link_id));
//remove
bdd.orders.where(x => IdsToRemove.Contains(x.link_id)).ForEach(x => bdd.Remove(x));
//add
foreach(var order in newList.Where(x -> IdsToAdd.Contains(x.link_id))
{
bdd.Orders.Attach(order);
bdd.Entries(order).EntityState = EntityState.Added;
}
//update
foreach(var order in newList.Where(x -> IdsToUpdate .Contains(x.link_id))
{
bdd.Orders.Attach(order);
bdd.Entries(order).EntityState = EntityState.Modified;
}
bdd.SaveChanges();
But i'm unconfortable with this, because in my mind, entity framework
should be able to do the work for me !
In fact, EF does the Work for you. Using the data context SaveChanges method EF should be able to save all your changes at once:
DbContext.SaveChanges()
For your convinience you can still override this method. Internally you should use something like this:
public override int SaveChanges()
{
var changeSet = ChangeTracker.Entries<IAuditable>();
if (changeSet != null)
{
foreach (var entry in changeSet.Where(c => c.State != EntityState.Unchanged))
{
entry.Entity.ModifiedDate = DateProvider.GetCurrentDate();
entry.Entity.ModifiedBy = UserName;
}
}
return base.SaveChanges();
}

Categories

Resources