Can't access number of items on navigational property - ef-code-first

I'm not sure if this issue is an Automapper-issue or a Entity Framework-issue.
I'm having trouble getting the number of products from the navigation property ProductCount in my viewmodel. The value returned is always "0". If the function worked, it would return "no" on a category with no products and "15" on a category with 15 products.
The viewmodel:
public class ViewModelProductCategory
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Title { get; set; }
public int SortOrder { get; set; }
public ViewModelProductCategory ParentCategory { get; set; }
public IEnumerable<ViewModelProductCategory> Children { get; set; }
public IEnumerable<ViewModelProduct> Products { get; set; }
public string ProductCount
{
get
{
return Products != null
? Products.Count().ToString()
: "no";
}
}
}
To display number of products:
#model List<MyStore.Models.ViewModels.ViewModelProductCategory>
#foreach (var item in Model)
{
#item.Title has #item.ProductCount product(s).
}
I have also tried to use #item.Products.Count() in the view, but that always returns 0.
This is how the viewmodel gets populated:
// Getting the categories
List<ProductCategory> DbCategories = _context.ProductCategories
.Include(e => e.Children).ToList().OrderBy(o => o.SortOrder)
.Where(e => e.ParentId == null).ToList();
// Mapping to ViewModel
List<ViewModelProductCategory> MapCategory =
_mapper.Map<List<ProductCategory>, List<ViewModelProductCategory>>(DbCategories);
The mapping:
CreateMap<ProductCategory, ViewModelProductCategory>()
.ForMember(dst => dst.Products, opt => opt.MapFrom(
src => src.ProductInCategory.Select(pc => pc.Product)));
ProductInCategory is a linking table between categories and products:
public class ProductInCategory
// A linking table for which products belongs to which categories
{
public int Id { get; set; }
public int ProductId { get; set; }
public int ProductCategoryId { get; set; }
public int SortOrder { get; set; }
// Nav.props.:
public Product Product { get; set; }
public ProductCategory ProductCategory { get; set; }
}
Why can't I get the number of products?
Edit
With the help of #IvanStoev in the comments, I changed the query to this:
//get all categories, so we have each and every child in Context
List<ProductCategory> DbCategories = _context.ProductCategories
.Include(e => e.Children)
.Include(e => e.ProductInCategory)
.ThenInclude(p => p.Product)
.ToList().OrderBy(o => o.SortOrder)
.Where(e => e.ParentId == null).ToList();
Now it works! Yay!

Related

Introducing FOREIGN KEY constraint 'FK_Product_User_UserId' on table 'Product' may cause cycles or multiple cascade paths

i cannot create database with ef core
error : Introducing FOREIGN KEY constraint 'FK_Product_User_UserId' on table 'Product' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
this is my product class
{
public Product()
{
}
public Guid UserId { get; set; }
public Guid CategoryId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string PhotoPath { get; set; }
public decimal Price { get; set; }
public Category Category { get; set; }
public User User { get; set; }
}
and
this is my user class
{
public User()
{
Payments = new HashSet<Payment>();
Categories = new HashSet<Category>();
Products = new HashSet<Product>();
}
public string Username { get; set; }
public Guid Password { get; set; }
public ICollection<Payment> Payments { get; set; }
public ICollection<Category> Categories { get; set; }
public ICollection<Product> Products { get; set; }
}
its mapping class
{
public ProductMap()
{
}
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(x => x.Id);
builder.Property(x => x.UserId).IsRequired();
builder.Property(x => x.CreatedDate).IsRequired();
builder.Property(x => x.Description).HasMaxLength(500);
builder.Property(x => x.IsActive).IsRequired();
builder.Property(x => x.Name).HasMaxLength(500).IsRequired();
builder.Property(x => x.PhotoPath).HasMaxLength(4000);
builder.Property(x => x.Price).HasColumnType("decimal(10,3)").IsRequired();
builder.HasOne(x => x.Category).WithMany(x => x.Products).HasForeignKey(x => x.CategoryId);
builder.HasOne(x => x.User).WithMany(x => x.Products).HasForeignKey(x => x.UserId);
}
}
and i cannot create database cause error like this posts title.
what can i do?
Thanks.
You are saying that each user has many Categories and each Category has many Products so you have to remove this line from the User class since it causing cycle path
public ICollection<Product> Products { get; set; }
And also fix the last line of the Configure method:
builder.HasOne(x => x.User).WithMany(x => x.Categories).HasForeignKey(x => x.UserId);

How can i access related data and convert foreign keys into objects for displaying their properties

I have
class User {
...
...
ICollection<Transaction> transactionsUserMade;
}
and
class Transaction {
int ID;
int userThatSentMoneyID;
int userToWhomHeSentMoneyID;
}
I'm trying to make profile page where user can see all transactions he made and to whom. I managed to relate users and transaction but I'm getting integer values, as i should by using
await _context.Users
.Include(u => u.transactionsUserMade)
.AsNoTracking()
.FirstOrDefaultAsync(u => u.ID == userId);
How can i turn those ID's to actual objects of Users so i could get their usernames and display them on Razor Page.
Found one solution. I tweaked Transaction class by adding User userThatRecievedMoney property. And after getting transactions from specific user i manually set that property.
foreach(var transaction in _user.transactionsUserMade)
{
transaction.userThatRecievedMoney = _context.Users
.Where(u => u.ID == transaction.userToWhomHeSentMoneyID).FirstOrDefault();
}
You can use Navigation Property to help you with that as long as you can modify those entity models User and Transaction.
public class UserEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<TransactionEntity> TransactionsAsSender { get; set; }
public List<TransactionEntity> TransactionsAsRecipient { get; set; }
}
public class TransactionEntity
{
public int Id { get; set; }
public double Amount { get; set; }
public string Note { get; set; }
// Foreign key to UserEntity
public int SenderId { get; set; }
public UserEntity Sender { get; set; }
// Foreign key to UserEntity
public int RecipientId { get; set; }
public UserEntity Recipient { get; set; }
}
Then you need to setup their relationships.
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<UserEntity>(b => {
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired();
b.Property(x => x.Email).IsRequired();
b.ToTable("User");
});
builder.Entity<TransactionEntity>(b => {
b.HasKey(x => x.Id);
b.Property(x => x.Amount).IsRequired();
// Configure relationships
b.HasOne(x => x.Sender)
.WithMany(u => u.TransactionsAsSender)
.HasForeignKey(x => x.SenderId);
b.HasOne(x => x.Recipient)
.WithMany(u => u.TransactionsAsRecipient)
.HasForeignKey(x => x.RecipientId);
b.ToTable("Transaction");
});
}
public DbSet<UserEntity> Users { get; set; }
public DbSet<TransactionEntity> Transactions { get; set; }
}
After their relationships are setup, you can easily query the related data via navigation properties.
For example, let's say you have view model called UserProfileViewModel and UserProfileTransactionViewModel to contain the information it needs for display purpose.
public class UserProfileViewModel
{
public int UserId { get; set; }
public string UserName { get; set; }
public string UserEmail { get; set; }
public IEnumerable<UserProfileTransactionViewModel> TransactionsAsSender { get; set; }
public IEnumerable<UserProfileTransactionViewModel> TransactionsAsRecipient { get; set }
}
public class UserProfileTransactionViewModel
{
public int TransactionId { get; set; }
public string Sender { get; set; }
public string Recipient { get; set; }
public string Amount { get; set; }
}
In the controller,
var user = _dbContext.Users
.AsNoTracking()
.Include(x => x.TransactionsAsSender)
.Include(x => x.TransactionsAsRecipient)
.SingleOrDefault(x => x.Id == userId);
if (user == null)
{
return NotFound();
}
var vm = new UserProfileViewModel
{
UserId = user.Id,
UserName = user.Name,
UserEmail = user.Email,
TransactionsAsSender = user.TransactionsAsSender
.Select(x => new UserProfileTransactionViewModel
{
TransactionId = x.Id,
Sender = x.Sender.Name,
Recipient = x.Recipient.Name,
Amount = x.Amount.ToString("c")
}),
TransactionsAsRecipient = user.TransactionsAsRecipient
.Select(x => new UserProfileTransactionViewModel
{
TransactionId = x.Id,
Sender = x.Sender.Name,
Recipient = x.Recipient.Name,
Amount = x.Amount.ToString("c")
})
};
return View(vm);
You could even have just a list of all transactions off UserProfileViewModel. You can combine TransactionsAsSender and TransactionsAsRecipient from UserEntity to fill the list.
Disclaim:
I wrote everything by hand and with my imagination :p

Filtering related tables in EF Core 2.0

I'm working at my first project in .Net Core 2.0. It is a simple blog system. I want to add search functionality based on post title and tags.
My entities:
public class Post
{
public int ID { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Body { get; set; }
public int CategoryID { get; set; }
public DateTime ReleaseDate { get; set; }
public string ImageName { get; set; }
public Category Category { get; set; }
public ICollection<PostTag> PostTags { get; } = new List<PostTag>();
}
public class PostTag
{
public int PostID { get; set; }
public int TagID { get; set; }
public Post Post { get; set; }
public Tag Tag { get; set; }
}
public class Tag
{
public int TagID { get; set; }
public string Name { get; set; }
public int Counter { get; set; }
public ICollection<PostTag> PostTags { get; } = new List<PostTag>();
public Tag()
{
Counter = 1;
}
So far I have come up with something like that. I joined the Tag table to be able to view all the tags for each post in IndexView.
public async Task<IActionResult> Index(int? page, string searchString)
{
IQueryable<Post> posts = _context.Posts
.OrderByDescending(post => post.ReleaseDate)
.Include(post => post.Category)
.Include(post => post.PostTags)
.ThenInclude(pt => pt.Tag);
//SEARCH
if (!String.IsNullOrEmpty(searchString))
{
posts = posts.Where(post => post.PostTags.Any(pt => pt.Tag.Name.Contains(searchString)) || post.Title.Contains(searchString));
//POPULARITY INCREESE
var tag = _context.Tags.SingleOrDefault(t => t.Name == searchString);
if (tag != null)
{
tag.Counter += 1;
_context.Update(tag);
_context.SaveChanges();
}
}
int pageSize = 4;
return View("Index", await PaginatedList<Post>.CreateAsync(posts.AsNoTracking(), page ?? 1, pageSize));
}
It's wroking but I would like to know if there is a simpler or better way.
And will .Where function work when i dont include related tables?
First of all the answer to your last question: As long as you didn´t send any request to the database, you don´t need Include() for filtering.
If you want to populate a list of entities, and you want to access Navigation-Properties e.g. while iterating over the list, you need to use Include().
If you want to avoid using Include(), you should select the values you need. This will avoid unexpected behaviour with NavigationProperties or something like this too. I would do something like this:
IQueryable<Post> posts = _context.Posts.OrderByDescending(post => post.ReleaseDate);
//SEARCH
if (!String.IsNullOrEmpty(searchString))
{
posts = posts.Where(post => post.PostTags.Any(pt => pt.Tag.Name.Contains(searchString)) || post.Title.Contains(searchString));
//POPULARITY INCREESE
var tag = _context.Tags.SingleOrDefault(t => t.Name == searchString);
if (tag != null)
{
tag.Counter += 1;
_context.Update(tag);
_context.SaveChanges();
}
}
int pageSize = 4;
return View("Index", await PaginatedList<Post>.CreateAsync(posts.AsNoTracking().Select(e=> new YourDisplayObject { DiplayValue = e.DbValue, DisplayNavProp = e.NavProp }, page ?? 1, pageSize));

Send info to the view from another table with navigation property?

This is my models
public class Product
{
[Key]
public int SerialNumber { get; set; }
public int PartNumber { get; set; }
public string Description { get; set; }
public virtual ICollection<Reading> Reading { get; set; }
}
public class Reading
{
[Key]
public int Id { get; set; }
public int SerialNumber { get; set; }
public int ReadingValue { get; set; }
public virtual Product Product { get; set; }
}
I can send all products to the view with
return View(db.Products.ToList().Where(product => product.CustomerID == Customer));
And I can get the latest ReadingValue if I know the Product SerialNumber
var LatestReading = db.Readings.OrderByDescending(m => m.Id).Where(s => s.SerialNumber == SerialNumber).Select(m => m.ReadingValue).FirstOrDefault();
How can I send all the products to the view with the latest ReadingValue for each product?
Create a new view model that will hold both the data:
public class FooViewModel
{
public List<Product> Products { get; set; }
public Reading LatestReading { get; set; }
}
Change your view to use the new model with:
#model FooViewModel
Then send them back in your controller:
var model = new FooViewModel();
model.Products = db.Products.ToList().Where(product => product.CustomerID == Customer);
model.LatestReading = db.Readings.OrderByDescending(m => m.Id).Where(s => s.SerialNumber == SerialNumber).Select(m => m.ReadingValue).FirstOrDefault();
return View(model);
Because you have Reading property in Products class, you can get the latest ReadingValue in the view:
foreach(Product product in Model)
{
var latestReadingValue = product.Reading.OrderByDescendin(m => m.Id).FirstOrDefault();
// do what you want here
}
but as hutchonoid points out the better option is creating a ViewModel for it, because having logic in the view is a bad practice, and it doesn't correspond to MVC pattern.

MVC 4 - many to many - get data only gets id of related table

Adding new data to the database is working with the many to many relationship.
Now I'm trying to get the project with all categories (category includes id and name). When I get all the project from my database then the associated categories are filled in, but only the id's.
CLASSES
public class Project
{
public Project() {
Categories = new HashSet<Category>();
}
public int ProjectID { get; set; }
[Display(Name = "Titel")]
public int CompanyID { get; set; }
public virtual Company Company { get; set; }
public virtual ICollection<Category> Categories { get; set; }
}
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public virtual ICollection<Project> Projects { get; set; }
}
CONTEXT
/*************ProjectS**************/
modelBuilder.Entity<Project>().HasKey(t => t.ProjectID);
modelBuilder.Entity<Project>().ToTable("Project", "freelauncher");
modelBuilder.Entity<Project>().Property(t => t.ProjectID).HasColumnName("project_id").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Project>().HasMany(p => p.Categories)
.WithMany(cat => cat.Projects)
.Map(pc =>
{
pc.ToTable("category_has_project");
pc.MapLeftKey("project_id");
pc.MapRightKey("category_id");
}
);
/*************CATEGORY**************/
modelBuilder.Entity<Category>().HasKey(t => t.CategoryID);
modelBuilder.Entity<Category>().ToTable("category", "freelauncher");
modelBuilder.Entity<Category>().Property(t => t.CategoryID).HasColumnName("category_id");
modelBuilder.Entity<Category>().Property(t => t.CategoryName).HasColumnName("category_name");
CONTROLLER
public ActionResult Projects()
{
IEnumerable<Project> projects = Adapter.ProjectRepository.Get();
return View();
}
REPOSITORY
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
What am I doing wrong to get the CategoryName in my projects?
The code works perfectly, the Category Names were missing from the db. (see comment above)

Resources