I have a Quote object, with a collection of QuoteAnswer objects. I also want a shortcut to the latest QuoteAnswer. So I modeled (irrelevant code ommitted for brevity):
public class Quote
{
public int Id { get; set; }
public ICollection<QuoteAnswer> Answers { get; set; }
public QuoteAnswer LatestAnswer { get; set; }
public int LatestAnswerId { get; set; }
}
public class QuoteAnswer
{
public int Id { get; set; }
public Quote Quote { get; set; }
public int QuoteId { get; set; }
/* Did not map this, not interested/not needed
* public Quote LastAnswerFor { get; set; }
* public int LastAnswerForId { get; set; }
*/
}
That's beacuse I want to be able to do this:
var quotes = context.Quotes
.Include(x => x.LatestAnswer)
.ToList();
Instead of this:
var quotes = context.Quotes
.Include(x => x.Answers)
.ToList();
foreach (var q in quotes)
{
var latestAnswer = q.Answers.OrderByDescending(x => x.Date).FirstOrDefault();
}
Which would obviously force me to load unecessary data.
The Problem
When I try to map this do database code (add a migration), I get a new property I don't know where it's comming from.
Generated migration code (parts ommitted for brevity):
CreateTable(
"dbo.QuoteAnswer",
c => new
{
Id = c.Int(nullable: false),
QuoteId = c.Int(nullable: false),
QuoteId1 = c.Int(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.Quote", t => t.QuoteId)
.ForeignKey("dbo.Quote", t => t.QuoteId1)
.Index(t => t.QuoteId)
.Index(t => t.QuoteId1);
AddColumn("dbo.Quote", "LatestAnswerId", c => c.Int());
CreateIndex("dbo.Quote", "LatestAnswerId");
AddForeignKey("dbo.Quote", "LatestAnswerId", "dbo.QuoteAnswer", "Id");
What's that QuoteId1 thing? I get the QuoteId, but I don't recognize QuoteId1.
How can I achive this mapping? Is this even supported in EF6?
First, it's possible. The explicit FK property should be removed:
public class Quote
{
public int Id { get; set; }
public string Data { get; set; }
public ICollection<QuoteAnswer> Answers { get; set; }
public QuoteAnswer LatestAnswer { get; set; }
}
and the new relationship should be mapped with fluent API:
modelBuilder.Entity<Quote>()
.HasOptional(e => e.LatestAnswer)
.WithOptionalDependent()
.Map(m => m.MapKey("LatestAnswerId"));
But I won't recommend you doing it because it would introduce a lot of maintenance problems - aside of the obvious need to keep it up-to-date, it would create circular FK dependency, so basically all CUD operations would be problematic (if working at all).
I think you are trying to solve the "loading unnecessary data" problem is a wrong way. You can achieve the same goal by using simple projection:
var quotesWithLatestAnswer = context.Quotes
.Select(q => new { Quote = q, LatestAnswer = q.Answers.OrderByDescending(a => a.Date).FirstOrDefault() })
.ToList();
Note that the code inside Select will be translated to SQL and executed in the database, returning only the data needed.
To return the latest answer as part of your entity, you can make mark it as unmapped:
public class Quote
{
public int Id { get; set; }
public string Data { get; set; }
public ICollection<QuoteAnswer> Answers { get; set; }
[NotMapped]
public QuoteAnswer LatestAnswer { get; set; }
}
and use a combination of LiNQ to Entities (SQL) and LINQ to Objects query:
var quotes = context.Quotes
.Select(q => new { Quote = q, LatestAnswer = q.Answers.OrderByDescending(a => a.Date).FirstOrDefault() })
.AsEnumerable() // end of db query
.Select(qi => { qi.Quote.LatestAnswer = qi.LatestAnswer; return qi.Quote; })
.ToList();
This way you'll have clean and easy to maintain relational database model as well as efficient retrieval of the data needed.
Related
I have 2 tables in my ASP.NET MVC project: store and product.
The relationship between these tables is many-to-many, so I also have a table StoreProduct.
I want to do CRUD operations on table product, but I need column StoreID from table StoreProduct.
How can I get the storeid from StoreProduct to do CRUD operation on product table?
here is how you'd do it if you want a many-to-many relationship with enums i.e. you want many user roles and work with enums moreover, you can't store it as a flag because you need to know about the roles/privileges without source code(on db side).
TLDR ;) You'd have to create a join table which contains about about who has what privilege(or roles if you want).
There is a Users table which has a list of privileges, a privilege table which has privilege definition i.e. Id, name. And a Join table which will have User and Privilege as it's key. If an entry against this user/privilege combination is present that means this user has this privilege/role.
The code:
//for enum
public enum UserPrivilegeId : int
{
AddProject = 0,
ModifyProject = 1,
DeleteProject = 2,
AddUser = 3,
ModifyUser = 4,
DeleteUser = 5
}
//User class
public record User
{
public User()
{
Privileges = new HashSet<Privilege>();
}
public int Id { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; }
public virtual ICollection<Privilege> Privileges { get; set; }
public virtual List<UserPrivilege> UserPrivileges { get; set; }
}
//Privilege Class
public record Privilege //note record is IMPORTANT here, because this forces it to compare by value, if you want to *use a class*, then make sure to override GetHashCode and Equals
{
public Privilege()
{
Users = new HashSet<User>();
}
public Privilege(UserPrivilegeId privilegeId, string privilegeName)
{
PrivilegeId = privilegeId;
PrivilegeName = privilegeName;
Users = new HashSet<User>();
}
[Key]
public UserPrivilegeId PrivilegeId { get; set; }
public string PrivilegeName { get; set; }
public virtual ICollection<User> Users { get; set; }
public virtual List<UserPrivilege> UserPrivileges { get; set; }
}
//and finally the UserPrivilege join class
public record UserPrivilege
{
public UserPrivilegeId PrivilageId { get; set; }
public Privilege Privilage { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
//The set-up in dbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Privilege>()
.HasKey(p => p.PrivilegeId);
modelBuilder.Entity<Privilege>()
.Property(p => p.PrivilegeId)
.HasConversion<int>();
modelBuilder.Entity<User>()
.HasMany(user => user.Privileges)
.WithMany(privilege => privilege.Users)
.UsingEntity<UserPrivilege>(
j => j
.HasOne(up => up.Privilage)
.WithMany(u => u.UserPrivileges)
.HasForeignKey(up => up.PrivilageId),
j => j
.HasOne(up => up.User)
.WithMany(p => p.UserPrivileges)
.HasForeignKey(up => up.UserId),
j =>
{
j.Property(u => u.PrivilageId).HasConversion<int>();
j.HasKey(u => new { u.PrivilageId, u.UserId });
});
//this adds definitions of privileges to the table
modelBuilder.Entity<Privilege>()
.HasData(
Enum.GetValues(typeof(UserPrivilegeId))
.Cast<UserPrivilegeId>()
.Select(p => new Privilege(p, p.ToString())));
base.OnModelCreating(modelBuilder);
}
Use it by creating a wrapper around it with a boolean on IsActive like this:
public class UserPrivelegesDTO
{
public UserPrivelegesDTO(UserPrivilegeId privilege, bool isActive)
{
this.PrivilegeId = privilege;
this.PrivilegeName = privilege.ToString();
this.IsActive = isActive;
}
public UserPrivilegeId PrivilegeId { get; set; }
public string PrivilegeName { get; set; }
public bool IsActive { get; set; }
}
If you want to convert from List<Privileges> to List<UserPrivilegeDTO>, you can
return await _context.Privileges.OrderBy(x => x.PrivilegeId).ToListAsync(cancellationToken);
To Convert back to List<Privileges>, simply
var privileges = _userPrivilegesViewModel.Privileges.Where(x => x.IsActive).Select(x => new Privilege(x.PrivilegeId, x.PrivilegeName));
If you want to check if the user has privilege
var user = _context.Users.Include(x => x.Privileges).FirstAsync(x => x.Id == 1);
if (request.Editor.Privileges.Any(p => p.PrivilegeId == UserPrivilegeId.ModifyUser))
return true;
When you want to update privileges
var PrivilegeChangeUser = await
_context.Users
.Include(user => user.Privileges)
.Include(user => user.UserPrivileges)
.FirstOrDefaultAsync(user => user.Id == request.UserId);
//**NOTE**you *need* to include the join table i.e. UserPrivileges in order replace the privileges, if you do not include it EF will try to add the privileges which already exist :(
//To update the privileges from an IEnumerable<UserPrivilegeIdEnum>
//first get the privileges objects and add that to users
var AllPrivileges =
await _context.Privileges
.Include(x => x.UserPrivileges)
.Include(x => x.Users)
.Where(x =>
request.Privileges
.Contains(x.PrivilegeId)
).ToListAsync(cancellationToken);
PrivilegeChangeUser.Privileges = AllPrivileges;
I have used an enum as the primary key, but you can of course just use a standard int too. For more information read this about how to configure a many-to-many relationship in efcore
I have question about select...
var applicationUser = unitOfWork.ApplicationUsers.GetAll().Include(i => i.ApplicationUserRoles).ThenInclude(i => i.ApplicationRole)
.Where(i => i.UserName.ToUpper() == userName.ToUpper())
.Select(i => new
{
i.Email,
i.FirstName,
i.LastName,
i.PhoneNumber,
i.ImageUrl,
i.JoinedDate,
i.DateOfBirth,
i.ApplicationUserRoles
})
.FirstOrDefault();
I cant get ApplicationRole how to use it in select ?
If you are using projection with Select you don't need to use Include. Just select the values. To get the Roles via the UserRoles you will need to Select to retrieve those:
var applicationUser = unitOfWork.ApplicationUsers.GetAll()
.Where(i => i.UserName.ToUpper() == userName.ToUpper())
.Select(i => new
{
i.Email,
i.FirstName,
i.LastName,
i.PhoneNumber,
i.ImageUrl,
i.JoinedDate,
i.DateOfBirth,
ApplicationRoles = i.ApplicationUserRoles.Select(x => x.ApplicationRole).ToList()
})
.FirstOrDefault();
You can further refine this by sub-selecting just the role details you need from the application role...
// ...
ApplicationRoles = i.ApplicationUserRoles.Select(x =>
x.ApplicationRole.Select(ar => new { ar.RoleId, ar.RoleName })).ToList()
I recommend using SingleOrDefault rather than FirstOrDefault if you are expecting at most 1 result. First/FirstOrDefault should always be used with an OrderBy/OrderByDescending condition to ensure predictable results.
If I can use for ApplicationRoles, it is working
.Select(i => new
{i.Email,i.FirstName,i.LastName,i.PhoneNumber,i.ImageUrl,i.JoinedDate,i.DateOfBirth,
ApplicationRoles = i.ApplicationUserRoles.Select(x => x.ApplicationRole).ToList(),
})
I cant use for ApplicationUserRoles how is it work for it ?
public class ApplicationUserSummary
{
public DateTime JoinedDate { get; set; }
public string ImageUrl { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime? DateOfBirth { get; set; }
public ICollection<ApplicationRole> ApplicationRoles { get; set; }
public ICollection<ApplicationUserRole> ApplicationUserRoles { get; set; }
}
I have two tables. Parent table and link table that stores ids. Parent table have related data from itself. Here is Models:
public class People
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Relation> Relations { get; set; }
}
public class Relation
{
public int Id { get; set; }
public int ParentPeopleID { get; set; }
public int ChildPeopleID { get; set; }
public People People { get; set; }
}
Some test data
And mapped them like this
HasRequired(p => p.People).WithMany(p => p.Relations).HasForeignKey(p => p.ParentPeopleID);
When I'm calling
var Peoplelist = MyDbContext.People.Include(p=>p.Relations.Select(r=>r.People)).Where(p=>p.Id==1).ToList();
It returns itself not related People. In my case it should returns People with ids: 2,3,4 but returns three People with id 1
I can get what I need by MyDbContext.Relation but need by MyDbContext.People
What i'm doing wrong?
Is there another way to do that?
Ivan is correct. You should use join keyword to join the both tables as below. You will get the desired result (People with ids: 2,3,4) by using this query:
var Peoplelist = MyDbContext.Peoples.Join(MyDbContext.Relations, p => p.Id, r => r.ChildPeopleID,
(p, r) => new {p, r})
.Where(j => j.r.ParentPeopleID == 1).ToList();
Hope this will help.
I know my problem is really basic. If I write /api/category/1, I wanna list all Tales with the categoryId==1. I wrote this code, but it gives an error.
[HttpGet]
public IEnumerable<Tale> GetAllTalesByCategory(int id)
{
var tales = TaleService.FindAllTale().Select(x => new Tale
{
TaleId = x.TaleId,
TaleName = x.TaleName,
Content = x.Content,
VoicePath = x.VoicePath
}).Where(x => new Tale
{
x.Categories.Select(c => c.CategoryId).First() == id
});
}
Error:
Error 1 Cannot initialize type 'MasalYuvasi.Entities.Tale' with a collection initializer because it does not implement 'System.Collections.IEnumerable' D:\MvcProject\MasalYuvasi\MasalYuvasi\Controllers\DenemeController.cs 33 13 MasalYuvasi
Models:
public class Tale
{
public int TaleId { get; set; }
public string TaleName { get; set; }
public string Content { get; set; }
public string VoicePath { get; set; }
public virtual ICollection<Category> Categories { get; set; }
public Tale()
{
this.Categories = new List<Category>();
}
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public virtual ICollection<Tale> Tales { get; set; }
public Category()
{
this.Tales = new List<Tale>();
}
}
Try this:
[HttpGet]
public IEnumerable<Tale> GetAllTalesByCategory(int id)
{
var tales = TaleService.FindAllTale().Select(x => new Tale
{
TaleId = x.TaleId,
TaleName = x.TaleName,
Content = x.Content,
VoicePath = x.VoicePath
}).Where(x => x.Categories.Select(c => c.CategoryId).First() == id).ToList();
}
Fixed the where condition, and added .ToList().
The problem is that your code is using a collection initializer here:
new Tale
{
x.Categories.Select(c => c.CategoryId).First() == id
}
I'm not sure what this code is supposed to be doing, but as x.Categories.Select(c => c.CategoryId).First() == id will return a bool, I don't think this is doing what you want it to.
Based on your comment:
I want to list in the category of tales. Forexample I have 2 tales in CategoryId is 1. If I write "/api/category/1" ı want to list this 2 tales.
I think you are looking for something simpler than what you've got. You want to select Tales (represented by x) where Any of the categories have a CategoryId of id:
.Where(x => x.Categories.Any(c => c.CategoryId == id ));
Note that you can append .ToList() to the end of the where clause, as suggested by pjobs, but this may have a subtle effect on the behavior of your application. For more detail, see LINQ and Deferred Execution.
I'm trying to apply a filter (ApplyFilter) on a column that is Join (and projected) from another table. I have the following entity:
public class User
{
public virtual int Id { get; private set; }
public virtual string EMail { get; set; }
...
public virtual bool IsActive { get; set; }
public virtual int CompanyId { get; set; }
}
With a UserMap:
public class UserMap : ClassMap<User>
{
public UserMap()
{
Table("Users");
Id(x => x.Id, "UserId");
Map(x => x.EMail);
...
Join("CompanyUser", r =>
{
r.KeyColumn("UserId");
r.Map(x => x.IsActive);
r.Map(x => x.CompanyId);
r.Fetch.Join();
});
ApplyFilter<CompanyFilter>("this_1_.CompanyId = :companyId");
}
This actually works right now, but as you can see I'm having to include the alias name "this_1_" for the CompanyUser table in the filter... this doesn't sound correct, but if I leave the filter as is defined in the FilterDefinition class I get a Sql with a:
where this.CompanyId = ?p0
which is not mapped 'cos the CompanyId column comes from a different projection (CompanyUser as this_1_)
Is there a way to correct this and let nhibernate figure out the correct alias for the filter?
Thanks in advance for any help.
I think you must apply the filter on the join:
Join("CompanyUser", r =>
{
r.KeyColumn("UserId");
r.Map(x => x.IsActive);
r.Map(x => x.CompanyId);
r.Fetch.Join();
}).ApplyFilter<CompanyFilter>("CompanyId = :companyId");