EF Code First - Populating data in Many to Many - asp.net-mvc

I'm new to ASP.Net MVC and want to create a simple Blog project, therefore I have two entity posts and categories. each post can belong to many categories and each category can belong to many posts.
Models.cs
public class Category
{
[Key]
public int CategoryId { get; set; }
public int? ParentId { get; set; }
public virtual Category Parent { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public virtual ICollection<News> News { get; set; }
public Category()
{
News = new List<News>();
}
}
public class News
{
[Key]
public int NewsId { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public string Content { get; set; }
public string Source { get; set; }
public string SourceURL { get; set; }
public string Images { get; set; }
public string Password { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public DateTime? DeletedAt { get; set; }
public string CreatedBy { get; set; }
public string DeletedBy { get; set; }
public virtual PublishPeriod PublishPeriodId { get; set; }
public virtual ICollection<Category> Categories { get; set; }
public News()
{
Categories = new List<Category>();
}
}
ModelsMap.cs
public class CategoryMap:EntityTypeConfiguration<Category>
{
public CategoryMap()
{
Property(one => one.Title).HasMaxLength(100).IsRequired();
HasOptional(x => x.Parent).WithMany().HasForeignKey(x => x.ParentId);
}
}
public class NewsMap:EntityTypeConfiguration<News>
{
public NewsMap()
{
Property(x => x.CreatedBy).HasMaxLength(150);
Property(x => x.DeletedBy).HasMaxLength(150);
Property(x => x.Title).IsRequired().HasMaxLength(150);
Property(x => x.Summary).IsRequired();
Property(x => x.Content).IsRequired().HasColumnType("ntext");
Property(x => x.CreatedAt).HasColumnType("datetime");
Property(x => x.Password).IsOptional().HasMaxLength(128);
Property(x => x.DeletedAt).IsOptional();
Property(x => x.ModifiedAt).IsOptional();
HasMany(x => x.Categories).WithMany(x => x.News).Map(x =>
{
x.ToTable("NewsCategories");
x.MapLeftKey("News_NewsId");
x.MapRightKey("Category_CategoryId");
});
}
}
And DB Context
public DbSet<Category> Categories { get; set; }
public DbSet<News> News { get; set; }
public DbSet<PublishPeriod> PublishPeriod { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.Add(new CategoryMap());
modelBuilder.Configurations.Add(new NewsMap());
modelBuilder.Configurations.Add(new PublishPeriodMap());
I have a create view for posts that displays categories in a list with checkboxs and each checkbox value is category's ID. How can I insert or update posts and keep relation between post and categories.
NewsController
//
// POST: /Admin/News/Create
[HttpPost]
public ActionResult Create(News news, List<string> Category)
{
ViewBag.Categories = catRepository.All.OrderBy(x => x.Title);
if (ModelState.IsValid)
{
foreach (var item in Category)
{
news.AddCategory(catRepository.Find(int.Parse(item)));
}
news.CreatedAt = DateTime.Now;
news.CreatedBy = "M.Hesabi";
newsRepository.InsertOrUpdate(news);
newsRepository.Save();
return RedirectToAction("Index");
}
else
{
return View();
}
}
UPDATE: I created a method in News Model as #DanS said and edited my controller.

I'd recommend creating a method on the News class:
public void AddCategory(Category category) {
Categories.Add(category);
category.News.Add(this);
}
From your Controller you can then add each selected Category to the News instance, and then add the News to the DbContext prior to calling SaveChanges. This may depend, however, on how your repositories make use of the context -- in that if they open their own, instead of accessing a shared context, you might have to attach the categories to the News repository's context prior to saving. Hopefully this helps...
Update
IEntityChangeTracker error:
It appears as if MVCScaffolding uses a separate context for each repository. As mentioned, having separate contexts can lead to some additional required steps. As it stands now, your categories are tracked by Context A while your news is tracked by Context B-- You could detach/attach the category entities between the two contexts, but I'd say the recommended solution would be to change your repositories to accept a shared context through their constructors.
I'm assuming that you are instantiating the repositories in the controller's constructor, rather than using dependency injection, so you would modify your constructor code to do something like the following:
myContext = new YourContextClass();
catRepository = new CategoryRepository(myContext);
newsRepository = new NewsRepository(myContext);
You would then have to add the constructors to your repositories to assign the internal context property, and finally, adjust your controller to properly dispose of the context.
protected override void Dispose(bool disposing)
{
if (disposing)
myContext.Dispose();
base.Dispose(disposing);
}

Related

Load data from related entities using Entity Framework

I have two tables product and Images. They have a one-to-many relationship. I want to display product name and all its related images in the view, I am using repository pattern. I am new to MVC and Linq please help. Thanks in advance.
Here is my code....
public partial class tbl_Product
{
public int pro_id { get; set; }
public string pro_name { get; set; }
public string pro_desc { get; set; }
public string pro_model { get; set; }
public string pro_dimensions { get; set; }
public Nullable<int> pro_UnitsInStock { get; set; }
public Nullable<double> pro_price { get; set; }
public Nullable<double> pro_oldprice { get; set; }
public virtual ICollection<tbl_Images> tbl_Images { get; set; }
}
ProductRepository class:
public ProductDetail GetProductByID(int id)
{
var product = this.storeDB.tbl_Product.Where(x => x.pro_id == id).FirstOrDefault();
return product;
}
Just add an Include clause to load the related images:
public ProductDetail GetProductByID(int id)
{
var product = storeDB.tbl_Product
.Where(x => x.pro_id == id)
.Include(p => p.tbl_Images)
.FirstOrDefault();
return product;
}

ASP.NetCore OData DTO $expand navigation property results in empty array

I am using Entity Framework with OData to get data from my mysql database but I don't want to expose database entites to the user, so I've created some DTO's and map them with Automapper.
My Problem is that everything works fine except loading entities with $expand.
There are 2 Entities with 2 DTO's (in my project the dto's and domain models do not look the same, this is only for better reading):
public partial class Product
{
public string Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public virtual ICollection<ProductPrice> ProductPrices { get; set; }
}
public class ProductDTO
{
[Key]
public string Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
[ForeignKey("Category")]
public int CategoryId { get; set; }
public virtual ICollection<ProductPriceDTO> ProductPrices { get; set; }
public virtual CategoryDTO Category { get; set; }
}
public partial class Category
{
public int Id { get; set; }
public string Title { get; set; }
}
public class CategoryDTO
{
[Key]
public int Id { get; set; }
public string Title { get; set; }
}
public partial class ProductPrice
{
public string VendorId { get; set; }
public string ProductId { get; set; }
public decimal Price { get; set; }
public virtual Product Product { get; set; }
public virtual Vendor Vendor { get; set; }
}
public class ProductPriceDTO
{
[Key]
[ForeignKey("Vendor")]
public string VendorId { get; set; }
[Key]
[ForeignKey("Product")]
public string ProductId { get; set; }
public decimal Price { get; set; }
public virtual VendorDTO Vendor { get; set; }
public virtual ProductDTO Product { get; set; }
}
The models are created the following way:
public IEdmModel GetEdmModel(IServiceProvider serviceProvider)
{
var builder = new ODataConventionModelBuilder(serviceProvider);
builder.Namespace = "Functions";
//category
builder.EntitySet<CategoryDTO>("Categories").EntityType.Select().Filter().OrderBy().Expand().Count().Page();
//product
builder.EntitySet<ProductDTO>("Products").EntityType.Select().Filter().OrderBy().Expand().Count().Page();
return builder.GetEdmModel();
//productprice
builder.EntitySet<ProductPriceDTO>("ProductPrices").EntityType.Select().Filter().OrderBy().Expand().Count().Page();
}
Automapper profile:
public AutoMapperProfile()
{
CreateMap<Product, ProductDTO>()
.ForMember(dto => dto.Category, conf => conf.AllowNull())
.ForMember(dto => dto.ProductPrices, dest => dest.MapFrom(x => x.ProductPrices))
.ForMember(dto => dto.ProductPrices, dest => dest.ExplicitExpansion())
.ForMember(dto => dto.ProductPrices, conf => conf.AllowNull());
CreateMap<ProductPrice, ProductPriceDTO>()
.ForMember(dto => dto.Product, conf => conf.AllowNull())
.ForMember(dto => dto.Vendor, conf => conf.AllowNull());
}
Controller:
[Authorize]
[ODataRoutePrefix("Products")]
public class ProductsController : BaseODataController
{
private readonly IProductService ProductService;
private readonly IProductPriceService ProductPriceService;
public ProductsController(IMapper mapper, IProductService productService, IProductPriceService productPriceService) : base(mapper)
{
ProductService = productService;
ProductPriceService = productPriceService;
}
[AllowAnonymous]
[EnableQuery]
public IQueryable<ProductDTO> Get(ODataQueryOptions queryOptions)
{
var query = ProductService.QueryProducts();
string[] includes = GetExpandNamesFromODataQuery(queryOptions);
if (includes != null && includes.Length > 0)
{
return query.ProjectTo<ProductDTO>(null, includes);
}
return query.ProjectTo<ProductDTO>();
}
[AllowAnonymous]
[EnableQuery]
[ODataRoute("({key})")]
public IQueryable<ProductDTO> Get([FromODataUri] string key, ODataQueryOptions queryOptions)
{
var query = ProductService.QueryProducts().Where(x => x.Id.Equals(key));
string[] includes = GetExpandNamesFromODataQuery(queryOptions);
if (includes != null && includes.Length > 0)
{
return query.ProjectTo<ProductDTO>(null, includes);
}
return query.ProjectTo<ProductDTO>();
}
}
As I mentioned above every query works fine ($select, $filter, $orderBy, $count).
But when I call the following:
https://localhost:44376/odata/Products('631794')?$expand=Category
I get:
{"#odata.context":"https://localhost:44376/odata/$metadata#Products","value":[
as response.
In the output of Visual Studio there is a message:
No coercion operator is defined between types 'System.Int16' and 'System.Boolean'.
I think there must be something wrong with the Automapper profile. As I read somewhere .ProjectTo() with include parameters creates a Select to get the related data from the navigation property. I thought it is enough to create the relation with [ForeignKey] in the DTO.

Want to join two tables on primary key, display the results in one view

IQueryable<Product> product = objContext.Set<Product>().Include(p =>
p.Categories.Name).Where(p => p.Id == 2);
As per the current view, I'm getting an error. It says add other model with their properties. i.e. to include Category model and corresponding Name property.
#model IEnumerable<>crudOneToMany.Models.Product>
using viewmodel, is it possible to join two tables?
View
Error
A specified Include path is not valid. The EntityType 'crudOneToMany.Models.Category' does not declare a navigation property with the name 'Name'.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int CategoryId { get; set; }
public virtual Category Categories { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Product> Products { get; set; }
}
public class ProductDBContext : DbContext
{
public ProductDBContext()
: base("ProductDBContext")
{
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasRequired(o => o.Categories).WithMany(o => o.Products).HasForeignKey(o => o.CategoryId);
base.OnModelCreating(modelBuilder);
}
}
Your problem is here:
.Include(p => p.Categories.Name)
Instead you should write .Include(p => p.Categories)
This means that in output there will be loaded Categories navigation collection to product.
Name is simple string property (is not navigation property so it should not be included)
Here is the proposed ViewModel for you.
ProductViewModel.cs
public class ProductViewModel
{
public int Id { get; set; }
[Required(ErrorMessage = "required")]
public string ProductName { get; set; }
public Category Category { get; set; }
public ICollection<Category> Categories { get; set; }
}

EF7 Getting null values for entity's collection of entities which are many to many

I am getting null values for the collection of entities nested in my top entity. How do I properly write my LINQ query so that these values aren't null??
I am using Entity Framework 7 and MVC 6 Here are my classes:
My models:
public class WorkStation
{
public Id { get; set; }
public string Name{ get; set; }
public ICollection<PersonWorkStation> PersonWorkStations{ get; set; }
}
public class Person
{
public Id { get; set; }
public string FirstName { get; set; }
public ICollection<PersonWorkStation> PersonWorkStations{ get; set; }
}
public class PersonWorkStation
{
public int Id { get; set; }
public int PersonId { get; set; }
public Person Person { get; set; }
public int WorkStationId { get; set; }
public WorkStation WorkStation { get; set; }
}
My DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PersonWorkStation>()
.HasKey(op => new { op.Id });
modelBuilder.Entity<PersonWorkStation>()
.HasOne(pt => pt.Person)
.WithMany(p => p.PersonWorkStation)
.HasForeignKey(pt => pt.PersonId);
modelBuilder.Entity<PersonWorkStation>()
.HasOne(pt => pt.WorkStation)
.WithMany(t => t.PersonWorkStation)
.HasForeignKey(pt => pt.WorkStationId);
base.OnModelCreating(modelBuilder);
}
So with that being said, when I bring back a person, and look at the "PersonWorkStation"s collection, the WorkStation property is null. How can I bring back that entity?
Here is how I am retrieving the data:
var person = _context.Persons
.Include(p => p.PersonWorkStation)
.FirstOrDefault(p => p.Id == 1);
return person;
Again, the person.PersonWorkStations.Workstation entity is null for all items in the person.PersonWorkStations collection. How do I return this entity?
Thanks!
I have found the answer, I needed to add this line:
var person = _context.Persons
.Include(p => p.PersonWorkStation)
.ThenInclude(p => p.WorkStation)
.FirstOrDefault(p => p.Id == 1);
return person;

DataContext is missing properties in where statement

Could some tell me why I have no properties in my where statements after using a select statement for e.g.
db.Select(x => x.Lft).Where(x => x.DepartmentId == id);
// missing properties in the where clause
And could you help me correct my code to implement it please leave me an example of what to do to implement this thanks
Classes:
public class Department
{
public Department()
{
Products = new List<Product>();
}
public long DepartmentId { get; set; }
[Required(ErrorMessage="Please enter a name for the departments.")]
[DataType(DataType.Text)]
public string Name { get; set; }
[DataType(DataType.Text)]
[Required(ErrorMessage = "Please enter a valid url for the department.")]
public string Url { get; set; }
public int Lft { get; set; }
public int Rgt { get; set; }
public bool MenuItem { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
my DataContext class
internal class DepartmentsTypeConfiguration : EntityTypeConfiguration<Department>
{
public DepartmentsTypeConfiguration()
{
Property(department => department.DepartmentId)
.HasColumnName("DepartmentId")
.HasDatabaseGeneratedOption(databaseGeneratedOption: DatabaseGeneratedOption.Identity);
Property(department => department.Name)
.HasColumnName("Name")
.IsRequired();
HasKey(key => key.DepartmentId)
.HasMany(x => x.Products)
.WithRequired(x => x.Department)
.WillCascadeOnDelete(true);
}
}
public class LeapFrogDataContext : DbContext
{
public DbSet<Department> Departments { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<ProductSpecification> ProductSpecifications {get; set;}
public DbSet<Specification> Specifications { get; set; }
/**/
static LeapFrogDataContext()
//: base("name=LeapFrogDataConnection")
{
//Database.SetInitializer(new LeapFrogInitializer());
//Database.SetInitializer(new DropCreateDatabaseIfModelChanges<LeapFrogDataContext>());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new DepartmentsTypeConfiguration());
modelBuilder.Configurations.Add(new ProductsTypeConfiguration());
modelBuilder.Configurations.Add(new SpecificationsTypeConfiguration());
modelBuilder.Configurations.Add(new ProductSpecificationsTypeConfiguration());
base.OnModelCreating(modelBuilder);
}
}
db.Select(x => x.Lft) returns a list of int so in the where clause you will not access any property.
I guess you may switch select and where to achieve what you want. Assume db is the actual context.
db.Where(x => x.DepartmentId == id).Select(x => x.Lft)
That's a bit weird. Normally it should look like
db.context.Departments.Where(x => x.DepartmentId == id).Select(x => x.Lft)

Resources