How can I sort a query from a DbSet and include child items which should also be sorted.
Example:
I have a model for scheduling orders.
public class Order
{
public virtual int Id { get; set; }
public virtual int? SchedulingOrder { get; set; }
public virtual int? WeekId { get; set; }
public virtual Week Week { get; set; }
}
public class Week
{
public virtual int Id { get; set; }
public virtual DateTime StartDate { get; set; }
public virtual ICollection<Order> Orders { get; set; }
}
...
public DbSet<Week> Weeks { get; set; }
public DbSet<Order> Orders { get; set; }
Then an action method
public ActionResult ShopSchedule()
{
return View(db.Weeks.OrderBy(w => w.StartDate)
.Include(w => w.Orders.OrderBy(o => o.SchedulingOrder))
.ToList());
}
This doesn't work I think because of the nature of Include. Do I have to create a separate view model and map to it? Or is there some way to get around it right there in the query? There is some kind of syntax where people say new { left = right, etc } within the query?
related questions:
Ordering Entity Framework sub-items for EditorFor
C# Entity Framework 4.1 Lambda Include - only select specific included values
It's worth noting that the other 2 solutions here pull the data via SQL, then reorder things in memory, which is very wasteful in terms of performance during both the query and the post-processing. This solution gets things in one go via SQL alone, without the extra in-memory step.
It can be done as described in the second approach here:
How to order child collections of entities in EF
Like:
db.VendorProducts.Select(p =>
new { Product = p, S = p.Schedules.OrderBy(s => s.From) })
.FirstOrDefault(q => q.Product.Id == id).Product
So instead of an Include statement, you call up the related data in an anonymous object along with the original root data you were going to fetch, order it in that subquery and finally return the root data. The ordering remains intact. Twisted but it works.
To stick with your original code:
db.Weeks.Select(w => new { W = w, O = w.Orders.OrderBy(o => o.SchedulingOrder) })
.OrderBy(q => q.W.StartDate).Select(q => q.W);
You are right, you can't use orders in Include, it's not meant to work that way. But you could sort the results within the view using the OrderBy on the Orders collection. Also, you're returning a result directly, shouldn't it be return View(db.Weeks...);
Something like this should work :
public ActionResult ShopSchedule()
{
var vw = db.Weeks.OrderBy(w => w.StartDate)
.Include(w => w.Orders)
.ToList();
vw.Orders = vw.Orders.OrderBy(o => o.SchedulingOrder).ToList()
return view(vw);
}
Related
Say I have two models/tables.
public class ParentEntity
{
public Guid ID { get; set;}
public List<ChildEntity> ChildEntities { get; set; } // navigation property
}
public class ChildEntity
{
public Guid ID { get; set; }
public Guid ParentEntityID { get; set; } // foreign key
}
If I run a query like this:
var parentEntities = await _context.ParentEntities.Where(x => x.ChildEntities.Any()).ToListAsync();
From what I can tell (based mostly on experimentation), this query does not require explict hydration of ChildEntities (using .Include(x => x.ChildEntities)).
If I wanted to do something with the ChildEntities outside of the query/after the list is materialized, I would need to explicitly hydrate ChildEntities:
var parentEntities = await _context.ParentEntities.Include(x => x.ChildEntities)
.Where(x => x.ChildEntities.Any()).ToListAsync();
foreach (var parentEntity in parentEntities)
{
foreach (var childEntity in parentEntities)
{
// do something with childEntity
}
}
That's my understanding, anyway, and that's how it seems to work. However, I'm hoping to find some Microsoft documentation that mentions this explicitly. I haven't been able to find anything (all the search keywords I can think to use point me in the direction of Filtered Includes, which is NOT what I'm wondering about).
I want to be confident that my understanding is correct, and that I haven't just gotten "lucky" by the child entities already being hydrated from other queries in the same context.
Include needed ONLY for loading related entities and ONLY for such purpose. It has no affect on filter or projection. You can omit Include if you do not plan to load related entities.
In my database I have two entities: DbStatus and DbTask
public class DbStatus
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<DbTask> Tasks { get; set; }
}
public class DbTask
{
public int Id { get; set; }
public string Title { get; set; }
public bool Done { get; set; }
public int StatusId { get; set; }
public virtual DbStatus Status { get; set; }
}
In the OnModelCreating method, I establish the relation with the following code:
modelBuilder.Entity<DbStatus>()
.HasMany(s => s.Tasks)
.WithOne(t => t.Status)
.HasForeignKey(t => t.StatusId);
I also add some sample data in this method, setting the StatusId of newly created DbTasks.
Problem is, when I try to access the Status name of the DbTask using
task.Status.Name
I get a NullReferenceException.
Can anyone help me how to set up the relation properly?
IMPORTANT
For anyone reading this, the quickest solution (and the one fulfilling task-specific criterias) for this was provided Rob. However, you should read and implement the solution provided by Steve Py, for the reasons they also describe in their answer!
When getting your list of DbTasks from the database, you need to tell it to include the child Status objects.
Try something like this:
var tasks = dbContext.DbTasks
.Include(t => t.Status)
.ToList();
Setting a FK on an entity does not automatically cause that related entity to be loaded. When working with navigation properties I recommend avoiding declaring FK fields in entities and using shadow properties to avoid issues like this.
To update a status on a DbTask:
public ActionResult MarkTaskComplete(int taskId)
{
var completeStatus = _context.Statuses.Single(x => x.StatusId = Statuses.Complete);
// TODO: Validation that user can update task etc.
var task = _context.Tasks
.Include(x => x.Status)
.Single(x => x.TaskId == taskId);
task.Status = completeStatus;
_context.SaveChanges();
return Json(new { success = true; status = task.Status.Name } );
}
The issue with FK fields is that the behaviour can differ depending on whether you use the navigation property or the FK, and whether the navigation property is eager loaded or not. From the perspective of the Task, there are two sources of truth for the current Status, some code might check task.StatusId while others use task.Status.StatusId. These values could differ depending on one being updated without the other.
While this can mean a trip to the DB to fetch a status, fetching rows by ID is extremely fast, and also provides a validation that your methods are only using legal values.
In a GET request, I can create a mapping from my back-end model to a customized DTO with AutoMapper with ease. However, I have some concerns when using AutoMapper with POST requests.
Suppose a user orders a product online, he sends the server a POST request with some required data. The fact is, not every piece of data in the back-end model is sent by the user. Let's say the ID of the Order is a GUID which is generated automatically when the entry is inserted into the database; or maybe there are other properties which are auto-incremented. All of these cannot-be-mapped properties lead to a lot of .ForMember(dest => dest.myProperty, opt => opt.Ignore()) chains, and extra handling on the mapped instance after var mappedInstance = Mapper.Map<PostDTO, BackEndModel>(postDTO).
Is AutoMapper not designed for the aforementioned scenario? What is the practice for handling the model-mapping process if the back-end model is much more complex than the DTO?
Update
public class MultipleChoiceQuestion
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid MultipleChoiceQuestionId { get; set; }
[Required]
public string Question { get; set; }
[Required]
public ICollection<PossibleChoice> PossibleChoices { get; set; }
}
public class PossibleChoice
{
[Key, Column(Order = 1), ForeignKey("MultipleChoiceQuestion")]
public Guid MultipleChoiceQuestionId { get; set; }
[Key, Column(Order = 2)]
public int ChoiceIndex { get; set; }
[Required]
public string AnswerText { get; set; }
public MultipleChoiceQuestion MultipleChoiceQuestion { get; set; }
}
The user sends a request to create a new question. Only 2 fields are sent.
{
"Question": "How are you?",
"Answers": [
{ "Text": "I am fine." },
{ "Text": "Feeling bad." }
]
}
Properties that are missing at this stage:
MultipleChoiceQuestionId
Generated after the insertion
ChoiceIndex
Auto-incremented from 1 up to the number of answers available
Without manual mapping, how to handle this situation with AutoMapper?
1- Define your DTOs to be something like this:
public class MultipleChoiceQuestionDto
{
// This property could stay here, because you may need to use the same DTO for update (PUT),
// which means you need the Id to distinguish and validate the DTO data against the URL id
//public Guid MultipleChoiceQuestionId { get; set; }
public string Question { get; set; }
public ICollection<PossibleChoiceDto> PossibleChoices { get; set; }
}
public class PossibleChoiceDto
{
// This can go from this dto, because this DTO is a child dto for its parent.
//public Guid MultipleChoiceQuestionId { get; set; }
// This property could stay here, because you may need to use the same DTO for update (PUT),
// which means you need the Id to know which Choice was updated.
//public int ChoiceIndex { get; set; }
public string AnswerText { get; set; }
}
2- You create a mapping between the entity and the corresponding Dto like this, make sure you call this code from the global.asax file.
Mapper.CreateMap<MultipleChoiceQuestion, MultipleChoiceQuestionDto>();
Mapper.CreateMap<MultipleChoiceQuestionDto, MultipleChoiceQuestion>()
.ForMember(m => m.MultipleChoiceQuestionId, e => e.Ignore()); // you force automapper to ignore this property
Mapper.CreateMap<PossibleChoice, PossibleChoiceDto>();
Mapper.CreateMap<PossibleChoiceDto, PossibleChoice>()
.ForMember(m => m.MultipleChoiceQuestion, e => e.Ignore()) //
.ForMember(m => m.MultipleChoiceQuestionId, e => e.Ignore())
.ForMember(m => m.ChoiceIndex, e => e.Ignore());
3- In your controller.Post you need to map from the DTO to the entity and save the mapped entity to the database.
Now, the above solution will work for you for POST, however, you need to think about the PUT scenario and soon you will realize that you need the Ids to be included in the DTOs, and if you decided to do that then you need to revisit the mapping in point 2 and remove the Ignore code for the properties that you decided to include in the DTO.
Hope that helps.
I'm not sure where in your architecture you're using AutoMapper, but you could conceptually whitelist the properties before doing the automapping. For example, if you're in MVC and you're doing modelbinding, there are techniques (e.g. in the UpdateModel method) to include or exclude a list of properties.
I would like to set up a many to many relationship in ASP.NET MVC4.
The goal is to extend the default UserProfile class with a list of Timeline objects, which belong to the user.
A Timeline could be shared by multiple users, so the Timeline class should have an list of UserProfile objects aswell.
Timeline class:
namespace MvcApplication1.Models
{
public class Timeline
{
public int Id { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public List<UserProfile> Users { get; set; }
}
public class TimelineContext : DbContext
{
public DbSet<Timeline> Timelines { get; set; }
public DbSet<UserProfile> UserProfiles { get; set; }
// Added the following because I saw it on:
// http://www.codeproject.com/Tips/548945/Generating-Many-to-Many-Relation-in-MVC4-using-Ent
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Timeline>()
.HasMany(c => c.Users)
.WithMany(s => s.Timelines)
.Map(mc =>
{
mc.ToTable("TimelineOwners");
mc.MapLeftKey("TimelineId");
mc.MapRightKey("UserId");
});
}
}
}
UserProfile class (default class with an added property):
public class UsersContext : DbContext
{
public UsersContext()
: base("DefaultConnection")
{
}
public DbSet<UserProfile> UserProfiles { get; set; }
public DbSet<Timeline> Timelines { get; set; }
// Added the following because I saw it on:
// http://www.codeproject.com/Tips/548945/Generating-Many-to-Many-Relation-in-MVC4-using-Ent
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserProfile>()
.HasMany(c => c.Timelines)
.WithMany(s => s.Users)
.Map (mc =>
{
mc.ToTable("TimelineOwners");
mc.MapLeftKey("UserId");
mc.MapRightKey("TimelineId");
});
}
}
[Table("UserProfile")]
public class UserProfile
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string UserName { get; set; }
public List<Timeline> Timelines { get; set; }
}
I have a connection table with foreign keys:
When creating an instance of Timeline, the Users list is null:
Timeline timeline = db.Timelines.Find(id); // timeline.Users = null
Can somebody please enlighten me, how should I set this up working?
I'm totally new to ASP.NET MVC4.
Edit 1: I understand I should not extend UserProfile but create another class to store users. Once the many-to-many relationship works, I will refactor and go into that direction.
But first I would like to know why is it not working.
Edit 2:
The double context also caused problems, two databases were created for the two contexts and the pure join table was empty in one of them.
I suggest that you work through this article about the options how you can load navigation properties with Entity Framework. This is very basic knowledge which is important for every kind of relationship, not only many-to-many relationships.
Looking at that article you will find then that this line...
Timeline timeline = db.Timelines.Find(id);
...does not load any related entities. So, it's expected that timeline.Users is null, even if the entities are related in the database.
If you want to load the Users you can use eager loading:
Timeline timeline = db.Timelines.Include(t => t.Users)
.SingleOrDefault(t => t.Id == id);
This is a single database query. Or to enable lazy loading you have to mark your navigation properties as virtual:
public virtual List<UserProfile> Users { get; set; }
//...
public virtual List<Timeline> Timelines { get; set; }
You can then use your original code:
Timeline timeline = db.Timelines.Find(id); // first query
var users = timeline.Users; // second query
This will run two separate queries. The second is performed as soon as you access the navigation property for the first time.
BTW: Is there a reason why you have two context classes - TimelineContext and UsersContext? "Normally" you need only one context.
I'm not a fan of messing with the working of the internal userprofile. I would suggest creating your own user class, linking it to the simplemembershipprovider and adding functionality there. At max you'll extend the accountclasses a little to add more fields to register with, but that's about it.
Follow this extremely handy guide to get things working and let me know if you encounter an error.
I have the following classes:
public class CartItem
{
public long Id { get; set; }
public int Quantity { get; set; }
public Product Product { get; set; }
}
public class Product {
public long Id { get; set; }
public string Title { get; set; }
public decimal Price { get; set; }
}
I currently have the following configuration:
modelBuilder.Entity<CartItem>().HasRequired(x => x.Product).WithMany().Map(x => x.MapKey("ProductId"));
I am trying to ensure that whenever I retrieve a cartitem from the database there will be a join on the product table so I can access the product properties but not the other way around.
I basically want to be able to do:
string title = cartItem.Product.Title
using the configuration I have gives me an Object reference not set to an instance of an object exception.
Short answer: to solve your problem, make the Product property virtual.
In-depth:
First, you don't need a join to do this. EF works fine with lazy loading (you need the virtual modifier)
Second, you can fetch the Product eagerly, using the Include extension method. Example:
var cartItem = context.CartItems.Include(x => x.Product)
.Where(/*some condition*/).ToList();
...but you can't configure this to be the default behavior (nor is it a good idea usually)
Third, this is a many-to-one relationship, not one-to-one (a Product has many related CartItems)