LINQ. Why full load only the first time? - c#

I have an entity Contracts, ListKindWorks and KindWorks.
public partial class Contracts
{
public Contracts()
{
ListKindWorks = new HashSet<ListKindWorks>();
}
public int Id { get; set; }
...
public virtual ICollection<ListKindWorks> ListKindWorks { get; set; }
}
public partial class ListKindWorks
{
public int IdContract { get; set; }
public int IdKindWork { get; set; }
public virtual Contracts IdContractNavigation { get; set; }
public virtual KindWorks IdKindWorkNavigation { get; set; }
}
public partial class KindWorks
{
public KindWorks()
{
ListKindWorks = new HashSet<ListKindWorks>();
}
public int Id { get; set; }
public string Title { get; set; }
public virtual ICollection<ListKindWorks> ListKindWorks { get; set; }
}
And class Item
public class Item
{
public int Id { get; set; }
public string Value { get; set; }
}
I want to load related elements via method Load():
source = model.Contracts
.OrderByDescending(r => r.Id)
.Skip(Page * Size)
.Take(Size)
.Select(c => new ContractTableRow
{
IdContract = c.Id,
FullName = c.WorkerNavigation.FullName,
IdWorker = c.Worker,
...
// this code
KindWork = c.ListKindWorks
.Select(y => new Item
{ Id = y.IdKindWork, Value = y.IdKindWorkNavigation.Short })
.ToList(),
Subject = c.ListSubjects
.Select(y => new Item
{ Id = y.IdSubject, Value = y.IdSubjectNavigation.Short })
.ToList()
})
.ToList();
The first call of the method Load() (during load app) gives full elements. KindWork and Subject are not empty. But then when I change Page, KindWork and Subject are empty. The rest is always changing.
Why don't subsequent calls load this part?

Use variable source as IQueryable so every time you use .ToList() query will be executed against database so you will get your changed data.
IQueryable<Contracts> source = *...* // without .ToList()
This will save this as a query (without data, as it is not executed yet on db)
Every time you call .ToList() EF will execute on sql.
var smth = source.ToList();

Related

entity framework core nested object with self parentId

I have a treetable structure and this data comes to me from the frontend.
In this treetable structure, there is IssueActivity and IssueActivityDetail for details of this issue.
Now my question is, more than one IssueActivityDetail field can be added to this IssueActivity field. How can I do this on the c# ef core side?
I tried to do it with the logic of ParentId. My Entity structures are as follows. I did not add the parentId in FluenApi because I did not fully understand it.
My IssueActivity table.
public partial class IssueActivitiy
{
public int Id { get; set; }
public int IssueId { get; set; }
public byte Type { get; set; }
public short SubActivityNo { get; set; }
public string SubActivityTitle { get; set; }
public virtual Issue Issue { get; set; }
public virtual List<IssueActivitiyDetail> IssueActivitiyDetails { get; set; }
}
My IssueActivityDetail table.
public partial class IssueActivitiyDetail
{
public int Id { get; set; }
public int IssueActivityId { get; set; }
public short LineNo { get; set; }
public string Definition { get; set; }
public byte RoleId { get; set; }
public byte Medium { get; set; }
public string Explanation { get; set; }
public int? ParentId { get; set; }
public virtual IssueActivitiy IssueActivity { get; set; }
}
FluentApi Configuration.
public void Configure(EntityTypeBuilder<IssueActivitiy> modelBuilder)
{
modelBuilder.ToTable("IssueActivitiy");
modelBuilder.HasKey(a => a.Id);
modelBuilder.Property(e => e.SubActivityNo).HasComment("Sıra No");
modelBuilder.Property(e => e.SubActivityTitle).HasMaxLength(256).IsUnicode(false);
modelBuilder.Property(e => e.Type).HasDefaultValueSql("((1))").HasComment("1) Temel Aktivite\r\n2) Alternatif Aktivite\r\n3) İşlem İptal Aktivite");
modelBuilder.HasOne(d => d.Issue).WithMany(p => p.IssueActivitiys).HasForeignKey(d => d.IssueId).OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_Issue_IssueActivitiy_Id");
}
public void Configure(EntityTypeBuilder<IssueActivitiyDetail> modelBuilder)
{
modelBuilder.ToTable("IssueActivitiyDetail");
modelBuilder.Property(e => e.Definition).IsRequired().HasMaxLength(2048).IsUnicode(false).HasComment("Açıklama");
modelBuilder.Property(e => e.Explanation).HasMaxLength(2048).IsUnicode(false).HasComment("Açıklama");
modelBuilder.Property(e => e.IssueActivityId).HasComment("Konu Id");
modelBuilder.Property(e => e.LineNo).HasComment("Sıra No");
modelBuilder.Property(e => e.Medium).HasComment("Ortam (Excel, Mail vb.)");
modelBuilder.Property(e => e.RoleId).HasComment("Rol");
modelBuilder.Property(e => e.ParentId);
modelBuilder.HasOne(d => d.IssueActivity).WithMany(p => p.IssueActivitiyDetails).HasForeignKey(d => d.IssueActivityId).OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_IssueActivitiy_IssueActivitiyDetail_");
}
Web Api is also the place where I try to receive and process the data, but I played a lot and couldn't do it correctly.
var vIssueActivity = issueInfo.IssueActivitiyInfos
.Select(a => new IssueActivitiy
{
Type = a.Type,
SubActivityNo = a.SubActivityNo,
SubActivityTitle = a.SubActivityTitle,
IssueActivitiyDetails = a.IssueActivitiyDetailInfos
.Select(x => new IssueActivitiyDetail
{
LineNo = x.LineNo,
Definition = x.Definition,
RoleId = vUser.RoleId,
Medium = x.Medium,
Explanation = x.Explanation,
IssueActivityDetail = new List<IssueActivitiyDetail> { }
}).ToList()
});
You don't need to keep ParentId property in IssueActivityDetail.
public partial class IssueActivitiy
{
...
public virtual List<IssueActivitiyDetail> IssueActivitiyDetails { get; set; }
}
public partial class IssueActivitiyDetail
{
...
public virtual IssueActivitiy IssueActivity { get; set; }
}
Your configuration looks not wrong.
Maybe you can use Include when getting the entity from db context.
var issueActivity = context.IssueActivities.Include(x => x.IssueActivityDetails).FirstOrDefault();
You can accomplish this by retrieving all the entries from the database. Then select the Root node and then let EF Core mapping do the rest.
public class TreeNode
{
public bool IsRoot { get; set; }
public int? ParentNodeId {get; set;}
public virtual List<TreeNode> ChildNodes {get; set;}
}
public class TreeNodeRepository
{
public async Task<TreeNode> GetTreeStructure()
{
var allNodes = await _context.TreeNodes.ToListAsync();
return allNodes.FirstOrDefault(t => t.IsRoot);
}
}
You could argue that ParentId == null would also imply that it's a parent node. this just makes the example given more tuitive imo.
You should consider performance, how many nodes will become an issue, is it exposed through a web-api and would iterating over the nodes be more efficient. So you wouldn't have to load the entire Tree into memory each time but let clients handle that instead.

Using LINQ SelectMany() to join multiple child tables without a navigation property

I kind of got confused on how to achieve what I want, and after searching in the internet, I think SelectMany() is the way to go, but I am getting lost on how to make this work (I have very weak understanding of how lambda expressions work I think)..
My goal is to be able to use LINQ to populate this class:
public class AttendanceList
{
public int AttendancePeriodId { get; set; } // from AttendancePeriod class
public int Activity { get; set; } // from DailyAttendance class
public string Name { get; set; } // from Employee class
public string Position { get; set; } // from Employee class
public string Department { get; set; } // from Employee class
}
I have a totally wrong and non-working code, but to illustrate, I wanna use something like:
var query = context.AttendancePeriod
.Include(i => i.DailyAttendance)
.Include(i => i.DailyAttendance).ThenInclude(ii => ii.Employee)
.Select(s => new AttendanceList
{
AttendancePeriodId = s.Id,
Activity = ...,
Name = ...,
Position = ...,
Department = ...
});
How do I use SelectMany() to achieve the above?
For reference, these are my classes:
public class AttendancePeriod
{
public int Id { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
public ICollection<DailyAttendance> DailyAttendances { get; set; }
}
public class DailyAttendance
{
public int Id { get; set; }
public Employee Employee { get; set; }
public TimeSpan TimeIn { get; set; }
public TimeSpan TimeOut { get; set; }
public string Activity { get; set;}
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Position { get; set; }
public string Department { get; set; }
}
Untested and without any null checking:
var query = context.AttendancePeriod
// .Include(i => i.DailyAttendance)
// .Include(i => i.DailyAttendance).ThenInclude(ii => ii.Employee)
.SelectMany(s => s.DailyAttendances.Select(a =>
new AttendanceList
{
AttendancePeriodId = s.Id,
Activity = a.Activity ,
Name = a.Employee.Name,
Position = a.Employee.Position,
Department = a.Employee.Department,
}));
Maybe you are looking for this
First step get a flat list of all DailyAttendances
.SelectMany(x => x.DailyAttendances)
Now transform those into AttendanceList
.Select(x => new AttendanceList
{
//x is of type `DailyAttendance`
AttendancePeriodId = x.AttendancePeriod.Id,
Activity = x.Activity,
Name = x.Employee.Name,
Position = x.Employee.Position,
Department = x.Employee.Department,
}
If DailyAttendance doesn't have a member for AttendancePeriod you could do the following, instead of
.SelectMany(x => x.DailyAttendances)
use this, this will create a tuple contains x = AttendancePeriod and y = DailyAttendance
.SelectMany(x => x.DailyAttendances.Select(y => (x, y))
and now transform it to this
.Select(x => new AttendanceList
{
//x is of type `ValueTuple<AttendancePeriod, DailyAttendance>`
//x.Item1 is of type AttendancePeriod
//x.Item2 is of type DailyAttendance
AttendancePeriodId = x.Item1.Id,
Activity = x.Item2.Activity,
Name = x.Item2.Employee.Name,
Position = x.Item2.Employee.Position,
Department = x.Item2.Employee.Department,
}

Select from self-referencing table and convert to viewmodel

How can i select all levels of a self-referencing table as a view model. if max level was 2 or 3 then i can do that by calling Select multiple times but i have 4-5 level menus and i think there should be a better solution for doing that and select all levels.
this is my viewmodel:
public class MenuViewModel
{
public MenuViewModel()
{
Childs = new HashSet<MenuViewModel>();
}
public int Id{ get; set; }
public string Title { get; set; }
public string Url { get; set; }
public ICollection<MenuViewModel> Childs { get; set; }
}
and this is my Menu class:
public class Menu
{
public Menu()
{
Childs = new HashSet<Menu>();
}
public int Id{ get; set; }
public string Title { get; set; }
public string Url { get; set; }
public string Description { get; se; }
public byte[] Icon { get; set; }
public int Order { get; set; }
public ICollection<Menu> Childs { get; set; }
}
var viewModel = _dataContext.Menus
.Select(x => new MenuViewModel
{
Id = x.Id,
Title = x.Title,
Child = ???
}
.ToList();
When you are using EF , you can do like following way:
public class BlogComment
{
public int Id { set; get; }
[MaxLength]
public string Body { set; get; }
public virtual BlogComment Reply { set; get; }
public int? ReplyId { get; set; }
public ICollection<BlogComment> Children { get; set; }
}
using (var ctx = new MyContext())
{
var list = ctx.BlogComments
//.where ...
.ToList() // fills the childs list too
.Where(x => x.Reply == null) // for TreeViewHelper
.ToList();
}
with this way you don't need to use recursive queries but As far as I know,when use view model for fetch data , the dynamic proxy of EF Is destroyed.
about above example:
just select one list of comments and with
.Where(x=>x.Reply==null).Tolist()
EF fill children property of Comments.
Reference
Assuming that Id property is unique you can do it in two passes:
Create viewmodel items without children, but with associated children ids. From that data create the Dictionary that will allow you to get any viewmodel by its id. Values in this dictionary will be the created viewmodels alongside their children ids.
For each viewmodel item get the associated view model items using the children ids.
Something like:
var tempModels = _dataContext
.Menus
.Select(menu => new
{
childrenIds = menu.Childs.Select(item => item.Id).ToArray(),
viewModel =
new MenuViewModel
{
Id = menu.Id,
Title = menu.Title
}
})
.ToDictionary(
keySelector: item => item.viewModel.Id);
var viewModels = tempModels
.Select(kv =>
{
var viewModel = kv.Value.viewModel;
viewModel.Childs = kv
.Value
.childrenIds
.Select(childId =>
tempModels[childId].viewModel)
.ToList();
return viewModel;
})
.ToList();
for depth problem you can use one int property like Depth in your Model then you can fetch data like this :
public class BlogComment
{
public int Id { set; get; }
[MaxLength]
public string Body { set; get; }
public int Depth{get;set}
public virtual BlogComment Reply { set; get; }
public int? ReplyId { get; set; }
public ICollection<BlogComment> Children { get; set; }
}
using (var ctx = new MyContext())
{
var list = ctx.BlogComments
.Where(a=>a.Depth<2)
.ToList() // fills the childs list too
.Where(x => x.Reply == null) // for TreeViewHelper
.ToList();
}
for using viewModel in this senario , I Test with AutoMapper,but when select data with viewModel , the dyamic proxy that EF generate is Destroyed .
Please Note this Issue

cannot be called with instance of type 'System.Data.Entity.Core.Objects.ObjectQuery

I want to Find Username by userId
this code snippet working
Discussion_CreateBy = db.AspNetUsers.Find(discussion.CreatedBy).UserName,
and this once not working in following controller class
Comment_CreateBy = db.AspNetUsers.Find(c.CreatedBy).UserName,
this is my model classes
public class DiscussionVM
{
public int Disussion_ID { get; set; }
public string Discussion_Title { get; set; }
public string Discussion_Description { get; set; }
public Nullable<System.DateTime> Discussion_CreateDate { get; set; }
public string Discussion_CreateBy { get; set; }
public string Comment_User { get; set; }
public IEnumerable<CommentVM> Comments { get; set; }
}
public class CommentVM
{
public int Comment_ID { get; set; }
public Nullable<System.DateTime> Comment_CreateDate { get; set; }
public string Comment_CreateBy { get; set; }
public string Comment_Description { get; set; }
}
this is whole controller class
public ActionResult Discussion_Preview()
{
int Discussion_ID = 1;
var discussion = db.AB_Discussion.Where(d => d.Discussion_ID == Discussion_ID).FirstOrDefault();
var comments = db.AB_DiscussionComments.Where(c => c.Discussion_ID == Discussion_ID);
DiscussionVM model = new DiscussionVM()
{
Disussion_ID = discussion.Discussion_ID,
Discussion_Title = discussion.Discussion_Name,
Discussion_Description = discussion.Discussion_Name,
Discussion_CreateBy = db.AspNetUsers.Find(discussion.CreatedBy).UserName,
Discussion_CreateDate = discussion.CreatedDate,
Comments = comments.Select(c => new CommentVM()
{
Comment_ID = c.Comment_ID,
Comment_Description = c.Comment_Discription,
Comment_CreateBy = db.AspNetUsers.Find(c.CreatedBy).UserName,
Comment_CreateDate = c.CreatedDate
})
};
return View(model);
}
Getting following error
Method 'Project.Models.AspNetUser Find(System.Object[])' declared on type 'System.Data.Entity.DbSet1[Project.Models.AspNetUser]' cannot be called with instance of type 'System.Data.Entity.Core.Objects.ObjectQuery1[Project.Models.AspNetUser]'
Discussion_CreateBy = db.AspNetUsers.Find(discussion.CreatedBy).UserName
Works because discussion is an in-memory object because you are executing a query by calling FirstOrDefault on it:
var discussion = db.AB_Discussion.Where(d => d.Discussion_ID == Discussion_ID).FirstOrDefault();
On the other hand in the following statement:
db.AspNetUsers.Find(c.CreatedBy).UserName
c is not queried yet because
db.AB_DiscussionComments.Where(c => c.Discussion_ID == Discussion_ID)
returns an IQueriable and not the actual collection of comments
The easiest way to fix it is to bring all your comments into memory (since you are anyway need them all) :
var comments = db.AB_DiscussionComments.Where(c => c.Discussion_ID == Discussion_ID).ToList();

how to assign collection of classes to a new viewmodel

I have this scenario:
I want to make a ViewModel with the property that I only want but my issue is with the Collections. So here's an example:
Main classes:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Order> Orders { get; set; }
// remove some code for brevity
}
public class Order
{
public int Id { get; set; }
public string ItemName { get; set; }
public int CustomerId { get; set; }
public virtual Customer Customer { get; set; }
// remove some code for brevity.
}
View Models:
public class OrderVM
{
public string ItemName { get; set; }
}
public class CustomerVM
{
public string Name { get; set; }
public ICollection<OrderVM> Orders { get; set; }
}
Then I tried something like this:
_customerService.Include(c => c.Orders)
.Select(x => new CustomerVM
{
Name = x.Name,
Orders = x.Orders.Select(order => new OrderVM { ItemName = order.ItemName }) // but it says cannot convert implicitly from IEnumerable to ICollection
}
)
In a nutshell, how can I populate CustomerVM's properties? I only want to select that I want. Any thoughts? Really stuck here.
Linq .Select() generates IEnumerable<T> but your property is ICollection<T>. You can append .ToList() to the query to generate List<T> which is ICollection<T>.
customerService.Include(c => c.Orders)
.Select(x => new CustomerVM
{
Name = x.Name,
Orders = x.Orders
.Select(order => new OrderVM { ItemName = order.ItemName }).ToList()
}
Alternatively, if you do not specifically need Orders to be ICollection, then you could change your property definition to
public IEnumerable<OrderVM> Orders { get; set; }
just use ToList() to convert IEnumerable to ICollection. Not tested but it should work
customerService.Include(c => c.Orders)
.Select(x => new CustomerVM
{
Name = x.Name,
Orders = x.Orders.Select(order => new OrderVM { ItemName = order.ItemName }).ToList()
}

Categories