I have the following Entities on Entity Framework Core 2.2:
public class Post {
public Int32 Id { get; set; }
public string Type { get; set; }
public virtual Collection<File> Files { get; set; }
}
public class File {
public Int32 Id { get; set; }
public Int32 PostId { get; set; }
public String Name { get; set; }
public Byte[] Content { get; set; }
public virtual Post Post { get; set; }
}
I need to get the list of files Ids and Names of a Post without loading their Content into Memory.
IQueryable<Post> posts = _context.Posts.AsNoTracking();
posts = posts.Include(x => x.File);
var files = await posts
.Where(x => x.Type == "design")
// Remaining Query
I think the moment I use Include the files will be loaded into memory. No?
What is the correct way to get a list of Posts' Files Ids and Names without loading their Content into Memory?
I need to get the list of files Ids and Names of a Post without loading their Content into Memory.
What is the correct way to get a list of Posts' Files Ids and Names without loading their Content into Memory?
Once you said you want to get a Post and then said you want to get a list of Post.
So to get a Post with its files (only Id and Name) you can write your query as follows:
var post = await _context.Posts.Where(yourCondition).Select(p => new
{
p.Id,
p.Type
Files = p.Files.Select(f => new {f.Id,f.Name}).ToList()
}).FirstOrDefaultAsync();
And to get list of Posts with its files (only Id and Name) you can write your query as follows:
var posts = await _context.Posts.Where(yourCondition).Select(p => new
{
p.Id,
p.Type
Files = p.Files.Select(f => new {f.Id,f.Name}).ToList()
}).ToListAsync();
Note: If you need strongly typed then can write as follows:
Post post = await _context.Posts.Where(yourCondition).Select(p => new Post
{
Id = p.Id,
Type = p.Type
Files = p.Files.Select(f => new File {f.Id,f.Name}).ToList()
}).FirstOrDefaultAsync();
Related
I have the following 3 models:
class Owner
{
public Guid Id { get; set; }
public IList<AccessShare> AccessShares { get; set; } = new List<AccessShare>()
}
class AccessShare
{
public Guid OwnerId { get; set; }
public Guid AccessorId { get; set; }
}
class File
{
public Guid OwnerId { get; set; }
public Owner Owner { get; set; }
public string Filename { get; set; }
}
The purpose of the system is assign ownership over files to one user, and to only allow other users to see those entities when there is an AccessShare from Accessor to Owner.
I have the following code that I'm using to bring back a file:
Guid fileId = <code that gets the accessor id>
Guid accessorId = <code that gets the accessor id>
File? file = DBContext
.Set<File>()
.Where(e => e.Id == fileId)
.Where(e => e.Owner.Id == accessorId || e.Owner.AccessShares.Any(a => a.AccessorId == accessorId))
.Include(e => e.Owner)
.Include(e => e.Owner.AccessShares)
.FirstOrDefault();
The issue I'm getting is that if null is returned, I don't know it that's because there isn't a File entity with the given id, or if there isn't an access share that allows access.
If this was raw SQL I'd do a left join from Owners to AccessShares with the above condition, this would always give me back the file/owner if found, and then optionally any access shares that meet the criteria.
I can find examples of how to do it in SQL and in Linq, but I can't find any examples using the DbSet fluid style.
From what you describe the part you are having issue with is:
.Where(e => e.Owner.Id == accessorId || e.Owner.AccessShares.Any(a => a.AccessorId == accessorId))
This will only return the desired file If the file is owned by the user, or has an access share.
Now if you want to return a File if it exists and provide an indication to the user whether they have permissions to access it, that could be done via projection rather than returning entities:
For example if I have a File ViewModel/DTO:
public class FileViewModel
{
public int FileId { get; set; }
// any other fields about the file I might display/use...
public bool UserHasAccess { get; set; }
public bool UserIsOwner { get; set; }
}
then I can query and populate these computed values via EF:
FileViewModel? file = DBContext
.Set<File>()
.Where(e => e.Id == fileId)
.Select(e => new FileViewModel
{
FileId = e.FileId,
// other fields...
UserHasAccess = e.OwnerId == accessorId || e.Owner.AccessShares.Any(a => a.AccessorId == accessorId),
UserIsOwner = e.OwnerId == accessorId
}).SingleOrDefault();
This will return a file if it exists, and includes details that your view/logic can use to determine if the file can be accessed by the current user, and/or is owned by the current user. Note that we remove the extra Where condition, and we don't need Include to access those related members when projecting.
Alternatively computed properties like this can be added to the File entity to do the same thing, however for these properties to function would require that the Owner and AccessShares are eager loaded (/w Include) or can be lazy loaded, accepting the potential performance pitfalls that can come with that. IMHO Projection /w Select is almost always preferable as it can also improve the resource footprint and performance of the querying.
I ran into a problem while developing my small Blazor WASM app.
A part of my app is where users can create teams, and invite other users to join their team. The relevant Entity Classes is:
Team.cs
public class Team
{
[Key]
public Guid TeamID { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
public Guid? BadgeID { get; set; }
public Guid TownID { get; set; }
public Guid StatisticsID { get; set; }
public Guid CaptainID { get; set; }
public List<AppUserDTO> Players { get; set; } = new();
}
When a User accepts an invitation he should be added to the List<AppUserDTO> Players List, I do this this way on the client side:
private async Task AcceptInvite()
{
Team.Players.Add(Player);
await TeamDataService.UpdateTeam(Team);
}
public async Task UpdateTeam(Team team)
{
var teamJson =
new StringContent(JsonSerializer.Serialize(team), Encoding.UTF8, "application/json");
await _httpClient.PutAsync("api/team", teamJson);
}
But I get the following exception on the server side when I'd like to save the changes to the server:
System.InvalidOperationException: The instance of entity type 'AppUserDTO' 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.
With the server-side code being:
public Team UpdateTeam(Team team)
{
var updatedTeam = _appDbContext.Teams.Include(t => t.Players).FirstOrDefault(t => t.TeamID == team.TeamID);
if (updatedTeam == null) return null;
updatedTeam.TeamID = team.TeamID;
updatedTeam.Name = team.Name;
updatedTeam.Abbreviation = team.Abbreviation;
updatedTeam.TownID = team.TownID;
updatedTeam.StatisticsID = team.StatisticsID;
updatedTeam.Players = team.Players;
updatedTeam.CaptainID = team.CaptainID;
_appDbContext.SaveChanges();
return updatedTeam;
}
The exception pops up at the _appDbContext.SaveChanges() method.
What I noticed is the following: When I add an Entity to an empty list and save it, I get no exception, but if the list already has Entities I get this error.
What would be the solution for this, I believe is quite common what I try to do, but I didn't find a solution anywhere for this.
When you execute:
var updatedTeam = _appDbContext.Teams
..Include(t => t.Players).FirstOrDefault(t => t.TeamID == team.TeamID);
... you are retrieving existing Players from the Db and _appDbContext is tracking them (by "ID").
Now, when you set Players:
updatedTeam.Players = team.Players;
... I suspect that team.Players includes Players that are already being tracked by the _appDbContext. Hence your error.
You could try:
List<Player> playersToAdd = team.Players.Except(updatedTeam.Players);
updatedTeam.AddRange(playersToAdd);
In this way, you are not adding duplicate players to the context that are already being tracked from the initial database retrieval.
What I would like to do is duplicate/copy my School object and all of its children/associations in EF Core
I have something like the following:
var item = await _db.School
.AsNoTracking()
.Include(x => x.Students)
.Include(x => x.Teachers)
.Include(x => x.StudentClasses)
.ThenInclude(x => x.Class)
.FirstOrDefaultAsync(x => x.Id == schoolId);
I have been reading up on deep cloning and it seems that I should be able to do just add the entity...so pretty much the next line.
await _db.AddAsync(item);
Then EF should be smart enough to add that entity as a NEW entity. However, right off the bat I get a conflict that says "the id {schoolId} already exists" and will not insert. Even if I reset the Id of the new item I am trying to add, I still get conflicts with the Ids of the associations/children of the school iteam.
Is anyone familiar with this and what I might be doing wrong?
I had the same problem too, but in my case EF core was throwing exception "the id already exists".
Following the answer of #Irikos so I have created method which clones my objects.
Here's example
public class Parent
{
public int Id { get; set; }
public string SomeProperty { get; set; }
public virtual List<Child> Templates { get; set; }
public Parent Clone()
{
var output = new Parent() { SomeProperty = SomeProperty };
CloneTemplates(output);
return output;
}
private void CloneTemplates(Parent parentTo, Child oldTemplate = null, Child newTemplate = null)
{
//find old related Child elements
var templates = Templates.Where(c => c.Template == oldTemplate);
foreach (var template in templates)
{
var newEntity = new Child()
{
SomeChildProperty = template.SomeChildProperty,
Template = newTemplate,
Parent = parentTo
};
//find recursivly all related Child elements
CloneTemplates(parentTo, template, newEntity);
parentTo.Templates.Add(newEntity);
}
}
}
public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
public virtual Parent Parent { get; set; }
public int? TemplateId { get; set; }
public virtual Child Template { get; set; }
public string SomeChildProperty { get; set; }
}
Then I just call DbContext.Parents.Add(newEntity) and DbContext.SaveChanges()
That worked for me. Maybe this will be useful for someone.
I had the same problem, but in my case, ef core was smart enough save them as new entities even with existing id. However, before realising that, I just made a copy constructor for all the items, created a local task variable containing only the desired properties and returned the copy.
Remove certain properties from object upon query EF Core 2.1
Let's say I have 3 models:
[Table("UserProfile")]
public class UserProfile //this is a standard class from MVC4 Internet template
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string UserName { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public int UserProfileId { get; set; }
[ForeignKey("UserProfileId")]
public virtual UserProfile UserProfile { get; set; }
}
Now, I'm trying to edit Post
[HttpPost]
public ActionResult Edit(Post post)
{
post.UserProfileId = context.UserProfile.Where(p => p.UserName == User.Identity.Name).Select(p => p.UserId).FirstOrDefault();
//I have to populate post.Category manually
//post.Category = context.Category.Where(p => p.Id == post.CategoryId).Select(p => p).FirstOrDefault();
if (ModelState.IsValid)
{
context.Entry(post.Category).State = EntityState.Modified; //Exception
context.Entry(post.UserProfile).State = EntityState.Modified;
context.Entry(post).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
And I'm getting ArgumentNullException.
Quick look into debug and I can tell that my Category is null, although CategoryId is set to proper value.
That commented out, nasty-looking trick solves this problem, but I suppose it shouldn't be there at all. So the question is how to solve it properly.
I would say it's something with EF lazy-loading, beacuse I have very similar code for adding Post and in debug there is same scenerio: proper CategoryId, Category is null and despite of that EF automagically resolves that Post <-> Category dependency, I don't have to use any additional tricks.
On edit method, EF has some problem with it, but I cannot figure out what I'm doing wrong.
This is working as intended. Your Post object is not attached to the Context, so it has no reason to do any lazy loading. Is this the full code? I don't understand why you need to set Category as Modified since you're not actually changing anything about it.
Anyway, I recommend you query for the existing post from the Database and assign the relevant fields you want to let the user modify, like such:
[HttpPost]
public ActionResult Edit(Post post)
{
var existingPost = context.Posts
.Where(p => p.Id == post.Id)
.SingleOrDetault();
if (existingPost == null)
throw new HttpException(); // Or whatever you wanna do, since the user send you a bad post ID
if (ModelState.IsValid)
{
// Now assign the values the user is allowed to change
existingPost.SomeProperty = post.SomeProperty;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
This way you also make sure that the post the user is trying to edit actually exists. Just because you received some parameters to your Action, doesn't mean they're valid or that the post's Id is real. For example, some ill intended user could decide to edit posts he's not allowed to edit. You need to check for this sort of thing.
UPDATE
On a side note, you can also avoid manually querying for the current user's Id. If you're using Simple Membership you can get the current user's id with WebSecurity.CurrentUserId.
If you're using Forms Authentication you can do Membership.GetUser().ProviderUserKey.
Supose the model as below:
class public Post
{
public int Id {get; set;}
public virtual ICollection<Comment> Comments {get;set;}
}
in the Posts/Index Page, I want to show a list of Post, with the Count of comments of each post (not total number of comments of all posts).
1: If I use
context.Posts.Include("Comments")
it will load the whole entity of all related commments , in fact I only need the Count of Comments.
2: If I get the count of each post one by one:
var commentCount = context.Entry(post)
.Collection(p => p.Comments)
.Query()
.Count();
that is a N+1 problem.
Any one knows the right way?
Thank you!
Do you need this for your presentation layer / view model? In such case create specialized ViewModel
public class PostListView
{
public Post Post { get; set; }
public int CommentsCount { get; set; }
}
And use query with projection:
var data = context.Posts
.Select(p => new PostListView
{
Post = p,
CommentsCount = p.Comments.Count()
});
And you are done. If you need it you can flatten your PostListView so that it contains Post's properties instead of Post entity.
What about something like this:
public class PostView
{
public String PostName { get; set; }
public Int32 PostCount { get; set; }
}
public static IEnumerable<PostView> GetPosts()
{
var context = new PostsEntities();
IQueryable<PostView> query = from posts in context.Posts
select new PostView
{
PostName = posts.Title,
PostCount = posts.PostComments.Count()
};
return query;
}
Then use something like this:
foreach (PostView post in GetPosts())
{
Console.WriteLine(String.Format("Post Name: {0}, Post Count: {1}", post.PostName, post.PostCount));
}
Should display the list as so:
Post name (12)
Post name (1)
Etc etc