public class ForumTopic
{
public Guid ForumTopicId { get; set; }
public Guid OwnerId { get; set; }
public Guid CategoryId { get; set; }
public DateTime CreatedDate { get; set; }
public string Topic { get; set; }
public bool IsSticky { get; set; }
public bool IsClosed { get; set; }
public int ViewCount { get; set; }
public int TotalComments { get; set; }
public Comment LastComment { get; set; }
}
I then have a Linq query and I need to figure out how to populate the LastComment and I can't create a new ForumTopic becuase Linq tells me that is breaking the rules...
IQueryable<ForumTopic> query = from topic in context.ForumTopics
join comment in context.Comments on topic.ForumTopicId equals comment.TargetId into topicComments
from lastComment in topicComments.DefaultIfEmpty().OrderByDescending(c => c.CreatedDate).Take(1)
orderby topic.IsSticky, topic.CreatedDate descending
select topic;
The query returns everything correct in SQL, however topic.LastComment is null.
Any ideas?
The main problem is you're not assigning the LastComment. Without a relationship established in the database, it has no idea how to fill that object.
You're going to need to manually assign the comment:
IQueryable<ForumTopic> query = from topic in context.ForumTopics
orderby topic.IsSticky, topic.CreatedDate descending
select new ForumTopic
{
ForumTopicId = topic.ForumTopicId,
OwnerId = topic.OwnerId,
// .. etc
LastComment = topic.Comments.OrderByDescending(c => c.CreatedDate).FirstOrDefault();
};
Obviously, I'm assuming you have a parent-child relationship between topic and comments. If you don't, you should reconsider how you're using linq :p
Related
I am using the Sakila Sample Database from MySql on a MySql server. The Diagram looks as follows.
The important tables are the store, inventory and film tables. The is a many-to-many relationship between the tables and the linker table is the inventory table.
I scaffolded this Database in a new dotnetcore project using EFCore 2.
I am trying to get a list of stores and their list of films.
The Entities are defined as follows:
Store
public class Store
{
public Store()
{
Customer = new HashSet<Customer>();
Inventory = new HashSet<Inventory>();
Staff = new HashSet<Staff>();
}
public byte StoreId { get; set; }
public byte ManagerStaffId { get; set; }
public short AddressId { get; set; }
public DateTimeOffset LastUpdate { get; set; }
public Address Address { get; set; }
public Staff ManagerStaff { get; set; }
public ICollection<Customer> Customer { get; set; }
public ICollection<Inventory> Inventory { get; set; }
public ICollection<Staff> Staff { get; set; }
}
Inventory
public partial class Inventory
{
public Inventory()
{
Rental = new HashSet<Rental>();
}
public int InventoryId { get; set; }
public short FilmId { get; set; }
public byte StoreId { get; set; }
public DateTimeOffset LastUpdate { get; set; }
public Film Film { get; set; }
public Store Store { get; set; }
public ICollection<Rental> Rental { get; set; }
}
Film
public partial class Film
{
public Film()
{
FilmActor = new HashSet<FilmActor>();
FilmCategory = new HashSet<FilmCategory>();
Inventory = new HashSet<Inventory>();
}
public short FilmId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public short? ReleaseYear { get; set; }
public byte LanguageId { get; set; }
public byte? OriginalLanguageId { get; set; }
public byte RentalDuration { get; set; }
public decimal RentalRate { get; set; }
public short? Length { get; set; }
public decimal ReplacementCost { get; set; }
public string Rating { get; set; }
public string SpecialFeatures { get; set; }
public DateTimeOffset LastUpdate { get; set; }
public Language Language { get; set;
public Language OriginalLanguage { get; set; }
public ICollection<FilmActor> FilmActor { get; set; }
public ICollection<FilmCategory> FilmCategory { get; set; }
public ICollection<Inventory> Inventory { get; set; }
}
My context looks as follows:
modelBuilder.Entity<Inventory>(entity =>
{
entity.ToTable("inventory", "sakila");
entity.HasIndex(e => e.FilmId)
.HasName("idx_fk_film_id");
entity.HasIndex(e => new { e.StoreId, e.FilmId })
.HasName("idx_store_id_film_id");
And lastly the repo looks as follows:
public IEnumerable<Store> GetStores()
{
return _context.Store.
Include(a => a.Inventory).
ToList();
}
Problem:
When I call this method from a Controller to get the list of stores I don´t get any json response on Postman. Yet if I debug into the list that is returned from the Controller I find the list of stores.
The problem is that the list contains:
store->inventory->film->store->inventory->film->store... Etc. Creating a circular dependency that fills up the allowed Process memory of the request.
Possible Solutions:
I think it has to do with the fact that on the Context both the Foreign Keys are defined as HasIndex instead of HasKey
entity.HasIndex(e => new { e.StoreId, e.FilmId })
.HasName("idx_store_id_film_id");
When I define it as HasKey then I get an Error:
'The relationship from 'Rental.Inventory' to 'Inventory.Rental' with
foreign key properties {'InventoryId' : int} cannot target the primary
key {'StoreId' : byte, 'FilmId' : short} because it is not compatible.
Configure a principal key or a set of compatible foreign key
properties for this relationship.'
To answer #hamzas comment, I did find a solution to this problem. I used EFCore to build the entities and the DBContext through scaffolding (DB First). As a best practice you should be using Models (Dtos) to represent the Data for the client. EFCore is very helpful in giving us the flexibility to access this M to N relationship however we want. This gives us the flexibility to represent this Data to the client however we want.
Whatever your use case might be. You have to convert the M to N relationship into an 1 to N model.
Use Case #1: You want to show all the movies for a specific store.
Solution
Step #1: You create a StoreDto (Model)
public class StoreDto
{
int StoreId { get; set; }
ICollection<FilmDto> Films { get; set; }
= new List<FilmDto> ();
}
Step #2: Create a FilmDto
public class FilmDto
{
int FilmId { get; set; }
int StoreId { get; set; }
string FilmName { get; set; }
}
Step #3: You provide a Mapping with auto mapper
public class MappingProfiles : Profile
{
public MappingProfiles()
{
CreateMap<Store, StoreDto>();
CreateMap<Film, FilmDto>();
}
}
Step #4: Query the data correctly, Unfortunately I don´t have this example anymore to test this code, so here is where you´ll have to experiment a bit
public Store GetFilmsForStore(byte StoreId)
{
return _context.Store.
Include(a => a.Inventory).
ThenInclude(i => i.Film)
ToList();
}
On the "Include" part you want to only get the Inventory entries where StoreId == Inverntory.StoreId and then Include the Films Object from the resulting list.
I hope you get the jist of it. You want to break up your m to n relationships and make them seem like 1 to m for your clients.
Using an example, I have the following two Entities. The OrderEntity contains a collection of OrderLineEntites
public class OrderEntity
{
public string Reference { get; set; }
public string Description { get; set; }
public bool Confirmed { get; set; }
[NotMapped]
public int OrderLineCount { get; set; }
[InverseProperty("Order")]
public virtual ICollection<OrderLineEntity> OrderLineEntity__OrderEntity { get; set; }
}
public class OrderLineEntity
{
public string Name { get; set; }
public string Description { get; set; }
}
Using the following code I can load all the OrderLineEntities for all confirmed orders.
DbSet<OrderEntity> orderEntity.Where(x => x.Confirmed).Include(x => x.OrderLineEntity__OrderEntity)
What I need to do is set the non-mapped OrderLineCount property with the Count of the OrderLine records (to save actually loading them).
So for each loaded Order I have a fully populated Entity including the [NotMapped] property with an empty OrderLine collection.
Advise would be appreciated :)
Thanks
You can do this, but you have to change your approach. You have to manually map the objects yourself:
var query = from a in context.Orders.Where(x => x.Confirmed)
select new OrderEntity
{
Reference = a.Reference,
Description = a.Description,
Confirmed = a.Confirmed,
OrderLineCount = a.OrderLineEntity__OrderEntity.Count
};
return query.ToList();
I'm developing a prototype platform using MVC in which users can make a profile and use that profile to make text posts, like a social media site. I have the following two tables in my database:
Profiles
public partial class Profile
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Profile()
{
this.Posts = new HashSet<Post>();
}
public int Id { get; set; }
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Country_ { get; set; }
public System.DateTime DoB { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Post> Posts { get; set; }
}
}
Posts
public partial class Post
{
public int Id { get; set; }
public string Text { get; set; }
public System.DateTime DATE { get; set; }
public int ProfileId { get; set; }
public virtual Profile Profile { get; set; }
}
}
In my ViewModel I have a list of Posts called PostList, which I want to populate with all of the Post records made by the user. So, I need to populate the list with all records where the ProfileId in Posts is equal to the Id of Profile, which is determined by whether or not the UserId in Profile is equal to the current user's Identity.
In short, I need:
Post List = Posts where ProfileId = Profiles.Id Where Profiles.UserId = CurrentUserId.
Any ideas? I've tried the following, but it's totally wrong:
var userId = User.Identity.GetUserId();
ViewModels.ProfileViewModel pVm = new ViewModels.ProfileViewModel();
pVm.PostList = db.Posts.Include(db.Profiles).Where(a => a.UserId == userId).ToList();
pVm.UserName = User.Identity.GetUserName();
return View(pVm);
You just walk the relationship in your query:
pVm.PostList = db.Posts.Where(a => a.Profile.UserId == userId);
It's unnecessary to use Include since Profile will automatically be joined to make the query.
I am writing a PhoneGap/Web/JS mobile application that uses the WebAPI and Entity Framework in the backend.
I have a class called Thing which references the User table 4 times (ChangedByUserId, CreatedByUserId etc. The User table is really large (30 user-related fields)
I want to pass as little data over each call as possible, but I need the User's Name for each of these UserID foreign keys. (this is the only information from the user record I need).
When I use the object graph in EF it returns the FULL user record for each foreign key, so a single Thing object becomes massively bloated. I don't want to make multiple calls to get the Thing POCO object and then the User's name by UserID.
What I really want to do is a sort of flattened DTO object which is just the Thing class below but with a string for each user name, e.g. CreatedByUserName, ChangedByUserName etc. Then I would return this DTO as my hydrated POCO object and the data would be small.
So my question is: How do I do this using Entity Framework? (limit related record's return data?)
public partial class Thing
{
public int ThingId { get; set; }
public int FromUserId { get; set; }
public int ToUserId { get; set; }
public string ThingText { get; set; }
public int StatusId { get; set; }
public int ChangedByUserId { get; set; }
public int CreatedByUserId { get; set; }
public virtual User FromUser { get; set; }
public virtual User ToUser { get; set; }
public virtual User CreatedByUser { get; set; }
public virtual User ChangedByUser { get; set; }
}
As you said, you need to flatten Thing
public class FlatThing
{
public int ThingId { get; set; }
public int FromUserId { get; set; }
public int ToUserId { get; set; }
public string ThingText { get; set; }
public int StatusId { get; set; }
public int ChangedByUserId { get; set; }
public int CreatedByUserId { get; set; }
public string FromUserName { get; set; }
public string ToUserName{ get; set; }
}
// assume you have your things
var flatThings = new List<FlatThings>;
foreach (Thing t in things)
flatThings.Add(new FlatThing{ ThingId = t.ThingId, FromUserId = t.FromUserId,
FromUserName = t.FromUser.Name .....});
return flatThings;
I am have some trouble getting Entity Framework to handle a many to many relationship in my data schema. Here is my model:
public class User
{
public int UserId { get; set; }
public int Username { get; set; }
public IEnumerable<Customer> Customers { get; set; }
...
}
public class Customer
{
public int CustomerId { get; set; }
...
}
public class CustomerUser
{
public int CustomerUserId { get; set; }
public int CustomerId { get; set; }
public int UserId { get; set; }
public DateTime CreatedTimestamp { get; set; }
...
}
Here is the mapping:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>().HasKey(u => u.UserId).ToTable("Users");
modelBuilder.Entity<Customer>().HasKey(c => c.CustomerId).ToTable("Customer");
modelBuilder.Entity<CustomerUsers>().HasKey(cu => cu.CustomerUserId).ToTable("CustomerUsers");
modelBuilder.Entity<CustomerUsers>()
.HasRequired(cu => cu.User)
.WithRequiredDependent()
.Map(m =>
{
m.ToTable("Users");
m.MapKey("CustomerUsers.UserId");
});
}
My database has a Users, Customers, and CustomerUsers table with columns that match the model.
I am trying to execute the following query:
result = (from u in context.Users
join customerUsers in context.CustomerUsers on u.UserId equals customerUsers.User.UserId
join customers in context.Customers on customerUsers.CustomerId equals customers.CustomerId into ps
select new
{
User = u,
Customers = ps
}).ToList().Select(r => { r.User.Customers = r.Customers.ToList(); return r.User; });
When I run the code, I get the following error:
The Column 'CustomerUserId' specified as part of this MSL does not exist in MetadataWorkspace
Can anyone see what is wrong with my approach?
Thanks!
I should note that I am intentionally trying to not include a reference to the CustomerUsers table from either the Customer or User class. The majority of the time, the payload of the CustomerUsers table is not important, only which customers are associated to which users. There are some reporting scenarios where the additional information in the join table is necessary, but since this is not the typical situation, I would like to avoid cluttering up the models by having this additional indirection.
Instead of trying to map this as many to many, map it as two one to many relationships. See the discussion of many to many join tables with payload in Many-to-Many Relationships in this tutorial:
http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/creating-a-more-complex-data-model-for-an-asp-net-mvc-application
For your model you will need probably two one-to-many relationships and the following navigation properties:
public class User
{
public int UserId { get; set; }
public int Username { get; set; }
// ...
public ICollection<CustomerUser> CustomerUsers { get; set; }
}
public class Customer
{
public int CustomerId { get; set; }
//...
public ICollection<CustomerUser> CustomerUsers { get; set; }
}
public class CustomerUser
{
public int CustomerUserId { get; set; }
public int CustomerId { get; set; }
public int UserId { get; set; }
public DateTime CreatedTimestamp { get; set; }
//...
public User User { get; set; }
public Customer Customer { get; set; }
}
And the following mapping:
modelBuilder.Entity<CustomerUser>()
.HasRequired(cu => cu.User)
.WithMany(u => u.CustomerUsers)
.HasForeignKey(cu => cu.UserId);
modelBuilder.Entity<CustomerUser>()
.HasRequired(cu => cu.Customer)
.WithMany(c => c.CustomerUsers)
.HasForeignKey(cu => cu.CustomerId);