Assuming I have Customer and Order objects, where one Customer can have many Orders (so the Order class has a CustomerId property), and I want to return a collection of all CustomerAndMostRecentOrder objects which are defined as follows:
public class CustomerAndMostRecentOrder
{
public Customer Customer { get; set; }
public Order MostRecentOrder { get; set; }
}
How would I write a Linq query which does this (I'm using Linq to SQL)?
You can use the following query:
from c in customers
select new CustomerAndMostRecentOrder
{
Customer = c,
MostRecentOrder = c.Orders.OrderByDescending(o => o.PurchaseDate).FirstOrDefault()
};
This will use a navigation property from customer to order. The MostRecentOrder is taken by ordering the Orders on some DateTime property and then loading the first one.
You will need to have an CreatedDate date in your Order table to get the most recent order. Then to get your CustomerAndMostRecentOrder object, do the following query:
from c in customers
join o in orders on c.ID equals o.CustomerID into co
select new CustomerAndMostRecentOrder
{
Customer = c,
MostRecentOrder = co.OrderByDescending(o => o.CreatedDate).FirstOrDefault()
}
public class CustomerAndMostRecentOrder
{
public CustomerAndMostRecentOrder(Customer customer, Order mostRecentOrder)
{
Customer = customer;
MostRecentOrder = mostRecentOrder;
}
public Customer Customer { get; set; }
public Order MostRecentOrder { get; set; }
}
public class Order
{
}
public class Customer
{
public IEnumerable<Order> GetOrders()
{
}
}
public static class UsageClass
{
public static void Sample(IEnumerable<Customer> allCustomers)
{
IEnumerable<CustomerAndMostRecentOrder> customerAndMostRecentOrders =
allCustomers.Select(customer => new CustomerAndMostRecentOrder(customer, customer.GetOrders().Last()));
}
}
As another alternative, you may want to take a look into the DataLoadOptions.AssociateWith discussed at http://msdn.microsoft.com/en-us/library/system.data.linq.dataloadoptions.associatewith.aspx. Simply set your requirements on the context and you don't need to worry about filtering the children at the query level.
Related
I have a custom SQL statement to get a max order of a customer. I don't have a table called MaxOrders - it's just a custom query.
I'm getting the customer records and related objects using Include
dbcontext.Customers.Include(x => x.MaxOrder)
I'd like to know how to configure the navigation property for this kind of scenario.
Customer class
public class Customer
{
public int Id { get; set;}
public string Name { get; set;}
public MaxOrder MaxOrder { get; set;}
}
MaxOrder class
public class MaxOrder
{
public int CustomerId { get; set;}
public decimal TotalAmount { get; set;}
public Customer Customer { get; set;}
}
DbContext
public DbSet<Customer> Customers { get; set; }
public DbSet<MaxOrder> MaxOrders{ get; set; }
ModelBuilder
modelBuilder.Entity<MaxOrder>()
.HasNoKey()
.ToView(null)
.ToSqlQuery(#"SELECT CustomerId, SUM(Amount) AS TotalAmount
FROM Orders O
WHERE Id = (SELECT MAX(Id)
FROM Orders
WHERE CustomerId = O.CustomerId)
GROUP BY CustomerId")
Disclaimer: What are you asking is not supported naturally by EF Core 5.0, hence the provided workaround most likely will break in future EF Core versions. Use it on your own risk, or use what is supported (mapping to real database view containing the desired SQL, as mentioned by other people).
Now, the problems. First, the entity type you want to map to SQL and also use in relationship cannot be keyless. It's simply because currently keyless entity types
Only support a subset of navigation mapping capabilities, specifically:
They may never act as the principal end of a relationship.
They may not have navigations to owned entities
They can only contain reference navigation properties pointing to regular entities.
Entities cannot contain navigation properties to keyless entity types.
In your case, Customer is violating the last rule by defining navigation property to keyless entity. But without it you won't be able to use Include, which is the end goal of all that.
There is no workaround for that limitation. Even if with some hackery you map the relationship and get correct SQL translation, still the navigation property won't be loaded because all EF Core related data loading methods rely on change tracking, and it requires entities with keys.
So, the entity must be "normal" (with key). There is no problem with that since the query has unique column which defines one-to-one relationship. However this hits another current EF Core limitation - you get NotImplemented exception for normal entities mapped to SqlQuery during the model finalization. Unfortunately this is inside static function used by many places inside the relational model finalization, which is also a static method, so virtually it's not possible to intercept and fix it from outside.
Once you know the problems (what is supported and what is not), here is the workaround. The supported mapping is normal entity to view. So we'll use that (ToView instead of failing ToSqlQuery), but instead of name will provide the SQL enclosed with () to be able to recognize and extract it from the associated EF Core metadata. Note that EF Core does not validate/care what are you providing them as names in ToTable and ToView methods - just wheter they are null or not.
Then we need to plug into EF Core query processing pipeline and replace the "view name" with the actual SQL.
Following is the implementation of the above idea (put it in some code file inside your EF Core project):
namespace Microsoft.EntityFrameworkCore
{
using Metadata.Builders;
using Query;
public static class InlineSqlViewSupport
{
public static DbContextOptionsBuilder AddInlineSqlViewSupport(this DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ReplaceService<ISqlExpressionFactory, CustomSqlExpressionFactory>();
public static EntityTypeBuilder<TEntity> ToInlineView<TEntity>(this EntityTypeBuilder<TEntity> entityTypeBuilder, string sql)
where TEntity : class => entityTypeBuilder.ToView($"({sql})");
}
}
namespace Microsoft.EntityFrameworkCore.Query
{
using System.Linq.Expressions;
using Metadata;
using SqlExpressions;
public class CustomSqlExpressionFactory : SqlExpressionFactory
{
public override SelectExpression Select(IEntityType entityType)
{
var viewName = entityType.GetViewName();
if (viewName != null && viewName.StartsWith("(") && viewName.EndsWith(")"))
{
var sql = viewName.Substring(1, viewName.Length - 2);
return Select(entityType, new FromSqlExpression("q", sql, NoArgs));
}
return base.Select(entityType);
}
private static readonly Expression NoArgs = Expression.Constant(new object[0]);
public CustomSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) : base(dependencies) { }
}
}
First two methods are just for convenience - one for adding the necessary plumbing and one for encoding the sql inside the name.
The actual work is inside the third class which replaces one of the standard EF Core services, intercepts the Select method which is responsinble for table/view/TVF expression mapping, and converts the special view names to SQL queries.
With these helpers in hand, you can use your sample model and DbSets as is. All you need is to add the following to your derived DbContext class:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// ...
optionsBuilder.AddInlineSqlViewSupport(); // <--
}
and use the following fluent configuration:
modelBuilder.Entity<MaxOrder>(builder =>
{
builder.HasKey(e => e.CustomerId);
builder.ToInlineView(
#"SELECT CustomerId, SUM(Amount) AS TotalAmount
FROM Orders O
WHERE Id = (SELECT MAX(Id)
FROM Orders
WHERE CustomerId = O.CustomerId)
GROUP BY CustomerId");
});
Now
var test = dbContext.Customers
.Include(x => x.MaxOrder)
.ToList();
will run w/o errors and generate SQL like
SELECT [c].[Id], [c].[Name], [q].[CustomerId], [q].[TotalAmount]
FROM [Customers] AS [c]
LEFT JOIN (
SELECT CustomerId, SUM(Amount) AS TotalAmount
FROM Orders O
WHERE Id = (SELECT MAX(Id)
FROM Orders
WHERE CustomerId = O.CustomerId)
GROUP BY CustomerId
) AS [q] ON [c].[Id] = [q].[CustomerId]
and more importantly, will correctly populate the Customer.MaxOrder property. Mission done :)
I was unable to get it working using ToSqlQuery as I received a NotImplementedException: SqlQuery exception when setting up the relationship between MaxOrder and Customer. Using a view, it worked without issue. If you are able to create a view, I suggest you do that.
MaxOrder needs a key, which is the FK to Customer and a 1:1 relationship is defined for MaxOrder:Customer. Replace the .ToView("vwMaxOrder") call with .ToSqlQuery(<body of view>) to reproduce the exception described above.
public class TestDbContext : DbContext
{
public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>()
.ToTable("Customer");
modelBuilder.Entity<Order>()
.ToTable("Order")
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.IsRequired();
modelBuilder.Entity<OrderItem>()
.ToTable("OrderItem")
.HasOne(oi => oi.Order)
.WithMany(o => o.Items)
.IsRequired();
modelBuilder.Entity<OrderItem>()
.HasOne(oi => oi.Item)
.WithMany()
.IsRequired();
modelBuilder.Entity<Item>()
.ToTable("Item");
modelBuilder.Entity<MaxOrder>()
.ToView("vwMaxOrder")
.HasKey(mo => mo.CustomerId);
modelBuilder.Entity<MaxOrder>()
.HasOne(mo => mo.Customer)
.WithOne(c => c.MaxOrder)
.HasForeignKey<MaxOrder>(mo => mo.CustomerId);
}
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Item> Items { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Order> Orders { get; set; }
public MaxOrder MaxOrder { get; set; }
}
public class Order
{
public int Id { get; set; }
public Customer Customer { get; set; }
public ICollection<OrderItem> Items { get; set; }
public DateTime Created { get; set; }
}
public class OrderItem
{
public int Id { get; set; }
public Order Order { get; set; }
public Item Item { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MaxOrder
{
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public decimal Value { get; set; }
}
View:
CREATE VIEW [dbo].[vwMaxOrder]
AS
select
c.Id CustomerId
, Value = MAX(OrderTotal.Value)
from
Customer c
inner join [Order] o
on c.Id = o.CustomerId
inner join
(
select
oi.OrderId
, Value = SUM(oi.Price * oi.Quantity)
from
OrderItem oi
group by
oi.OrderId
) OrderTotal
on o.Id = OrderTotal.OrderId
group by
c.Id
Demo program:
class Program
{
static void Main(string[] args)
{
using var db = CreateDbContext();
//AddCustomers(db);
//AddItems(db);
//AddOrders(db);
//AddOrderItems(db);
var customers = db.Customers
.Include(c => c.Orders)
.ThenInclude(o => o.Items)
.Include(c => c.MaxOrder)
.ToArray();
foreach(var customer in customers)
{
Console.WriteLine("----------------------");
Console.WriteLine($"Customer ID {customer.Id} max order amount: {customer.MaxOrder.Value}");
foreach (var order in customer.Orders)
{
var total = order.Items.Sum(oi => oi.Price * oi.Quantity);
Console.WriteLine($"Order ID {order.Id} total: {total}");
}
}
}
static TestDbContext CreateDbContext()
{
var opts = new DbContextOptionsBuilder<TestDbContext>()
.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Database=DemoDB;Trusted_Connection=True;")
.Options;
return new TestDbContext(opts);
}
static void AddCustomers(TestDbContext db)
{
db.Customers.Add(new Customer()
{
Name = "Customer A"
});
db.Customers.Add(new Customer()
{
Name = "Customer B"
});
db.SaveChanges();
}
static void AddItems(TestDbContext db)
{
db.Items.Add(new Item()
{
Name = "Item A",
});
db.Items.Add(new Item()
{
Name = "Item B",
});
db.SaveChanges();
}
static void AddOrders(TestDbContext db)
{
db.Orders.Add(new Order()
{
Created = DateTime.Now,
Customer = db.Customers.First(),
});
db.Orders.Add(new Order()
{
Created = DateTime.Now.AddDays(-1),
Customer = db.Customers.First(),
});
db.Orders.Add(new Order()
{
Created = DateTime.Now.AddDays(-2),
Customer = db.Customers.Skip(1).First(),
});
db.Orders.Add(new Order()
{
Created = DateTime.Now.AddDays(-3),
Customer = db.Customers.Skip(1).First(),
});
db.SaveChanges();
}
static void AddOrderItems(TestDbContext db)
{
var orders = db.Orders.Include(o => o.Items).ToArray();
var items = db.Items.ToArray();
for(var i = 0; i < orders.Length; ++i)
{
var order = orders[i];
for(var j = 0; j < items.Length; ++j)
{
order.Items.Add(new OrderItem()
{
Item = items[j],
Quantity = i + j + 1,
Price = 20 - i * 2 - j * 3,
});
}
}
db.SaveChanges();
}
}
Results:
----------------------
Customer ID 1 max order amount: 81.00
Order ID 1 total: 54.00
Order ID 2 total: 81.00
----------------------
Customer ID 2 max order amount: 111.00
Order ID 3 total: 100.00
Order ID 4 total: 111.00
I would propose more universal and easy to maintain solution:
public static class Associations
{
[Expandable(nameof(MaxOrderImpl)]
public static MaxOrder MaxOrder(this Customer customer)
=> throw new NotImplementedException();
private static Expression<Func<Customer, MaxOrder>> MaxOrderImpl()
{
return c => c.Orders.OrderByDescending(o => o.Id)
.Selec(o => new MaxOrder{ CustomerId = o.CustomerId, TotalAmount = o.Amount })
.FirstOrDefault();
}
}
Then you can use this extension in queries:
dbcontext.Customers.Select(x => new CustomerDto
{
Id = x.Id,
Name = x.Name,
MaxOrder = x.MaxOrder()
});
Queries are writtent in LINQ, extensions can be added easily and reused in other queries.
Such solution requires LINQKit and configuring your context:
builder
.UseSqlServer(connectionString)
.WithExpressionExpanding(); // enabling LINQKit extension
Hey I have two following classes:
public class Project
{
public int Id { get; protected set; }
public string ProjectName { get; protected set; }
public List<Task> TaskList{ get; protected set; }
}
public class Task
{
public int Id { get; protected set; }
public int ProjectId { get; protected set; }
public string Name { get; protected set; }
}
If I am using Entity Framework is it possible to get Project object from database (eg. by id) and in the same time join tasks list into [Project] TaskList property, or I should first get a project, and then tasks?
How it should be implemented properly?
If you described relationship between tables Project and Task as explained here
than you can directly just call your Project entity; it will come with a list of related Tasks; if you didn't described relationships than you have to create a join query by linq to etnity or use .Inlcude for relate entites to each other.
var tasks = (from items in db.Task
join projects in db.Project on items.ProejctId equals projects.Id
where 1==1 // you can add some other confitions to here
select new SelectItemsList() { //Items you want to select })
Do you require the Project class object like below code? without using .include<>
var res = (from p in db.ProjectTable
select new Project
{
Id = p.Id,
ProjectName = p.ProjectName,
TaskList = (from q in db.TaskTable
where q.ProjectId = p.Id
select q
).ToList()
}).ToList();
return res; //you will get List<Project> with their List<TaskList>
I have 2 tables in the database :
Table: Order (item_id)
Table: Item ( item_id)
When I'm doing the inner join in entity framework, as you can see below, I need to return in one list the result to manipulate this. Usually when I do the select in one single table , I return a LIST from the entity with the tables name, but I dont know how can I return a LIST when I have 2 or more entity , I mean, using inner join, I would like to return a List that I can manipulate in other class. When I use for only one entity, it is perfect and easy.
public List<????????> getTransdataByStatus(string status)
{
contenxt = new Finance_ManagementEntity();
var _result = (from a in contenxt.Orders
join b in contenxt.Items on a.item_id equals b.item_id
select new
{
a.order_numer,
a.total,
b.item_code,
b.item_qty
});
return _result;
}
I don't know how to return it !! I tried to use the .TOLIST(), but still coming "anonymous".
Thank you
First you need to create a custom type like
public class OrderItems
{
public int Order_numer { get; set; }
public int Total { get; set; }
public string Item_code { get; set; }
public int Item_qty { get; set; }
}
After then modify your function like
public List<OrderItems> getTransdataByStatus(string status)
{
contenxt = new Finance_ManagementEntity();
var _result = (from a in contenxt.Orders
join b in contenxt.Items on a.item_id equals b.item_id
select new OrderItems()
{
Order_numer= a.order_numer,
Total= a.total,
Item_code=b.item_code,
Item_qty=b.item_qty
}).ToList();
return _result;
}
I hope it will work for you.
You can create a compound model that has a property representing each entity.
public class CompoundModel
{
public Entities.Order { get; set; }
public Entities.Item { get; set; }
}
public List<CompoundModel> getTransdataByStatus(string status)
{
contenxt = new Finance_ManagementEntity();
var _result = (from a in contenxt.Orders
join b in contenxt.Items on a.item_id equals b.item_id
select new CompoundModel
{
Order = a
Item = b
});
return _result;
}
Alternatively, if you want to flatten your structure, you can create a class that only has four properties.
public class CompoundModel
{
public string OrderNumber { get; set; }
public int Total { get; set; }
public string ItemCode { get; set; }
public int ItemQuantity { get; set }
}
public List<CompoundModel> getTransdataByStatus(string status)
{
contenxt = new Finance_ManagementEntity();
var _result = (from a in contenxt.Orders
join b in contenxt.Items on a.item_id equals b.item_id
select new CompoundModel
{
OrderNumber = a.order_number,
Total = a.total,
ItemCode = b.item_code,
ItemQuantity = b.item_qty
});
return _result;
}
The problem with your code is this part:
select new // This will create an anonymous type
{
a.order_numer,
a.total,
b.item_code,
b.item_qty
}
As the select generates an anonymous type you will get a list of theses anonymous types as a result of the query. In order to get a list typed results, you need to specify the type in the select-clause:
select new TypeYouWantToReturn() // This will create an real type
{
PropA = a.order_numer, // You also need to specify the properties
PropB = a.total, // of the class that you want to assign
PropC = b.item_code, // the resulting values of the query.
PropD = b.item_qty
}
Now the result of the query will return a list of real types. You need to finally call .ToList() so you get a list instead of the IEnumerable that the select statement will return.
In order to get practice with Entity Framework, I am creating a C# WinForms project, and I used the answer to the question in this link to join and display tables in a datagridview:
Two entities in one dataGridView
I have searched for a clean way to save (to the SQL Server database) the changes made in the datagridview. Is there a good, fast, short, clean way? I have seen some ugly attempts, and even if they work, I am turned off by their ugliness.
With one table only (no joins), calling .SaveChanges works fine.
Here is the class set up to hold the joined fields:
public class CustAndOrders
{
// Customer table
public Int64 Cust_Id { get; set; }
public string Cust_Name { get; set; }
public DateTime Cust_BDay { get; set; }
//Order table
public Int64 Order_Id { get; set; }
public decimal Order_Amt { get; set; }
public DateTime Order_Date { get; set; }
// OrderDetail table
public Int64 Item_Number { get; set; }
public decimal Item_Amt { get; set; }
}
Here is the code to display the joined info in a datagridview. Obviously, the SaveChanges call does not work...yet
AWModel.TestJLEntities dc;
private void button1_Click(object sender, EventArgs e)
{
dc = new AWModel.TestJLEntities();
var query = from c in dc.Customers
join o in dc.Orders on c.CustId equals o.CustId
join od in dc.OrderDetails on o.OrderId equals od.OrderId
orderby o.OrderDate ascending, c.CustId, o.OrderAmount
select new CustAndOrders
{
Cust_Id = c.CustId,
Cust_Name = c.CustName,
Cust_BDay = c.CustBday,
Order_Id = o.OrderId,
Order_Amt = o.OrderAmount,
Order_Date = o.OrderDate,
Item_Number = od.ItemNumber,
Item_Amt = od.ItemAmount
};
var users = query.ToList();
dataGridView1.DataSource = users;
}
private void button2_Click(object sender, EventArgs e)
{
try
{
dc.SaveChanges();
}
catch
{
MessageBox.Show("Error saving changes");
}
}
EF is not seeing your changes because your data grid view is not bound to the EF entities, but rather an object populated from it's entities:
select new CustAndOrders
{
Cust_Id = c.CustId,
Cust_Name = c.CustName,
Cust_BDay = c.CustBday,
Order_Id = o.OrderId,
Order_Amt = o.OrderAmount,
Order_Date = o.OrderDate,
Item_Number = od.ItemNumber,
Item_Amt = od.ItemAmount
};
One solution I can think of is to compose CustAndOrders of the entities themselves:
public class CustAndOrders
{
public Customer Customer { get; set; }
public Order Order { get; set; }
public OrderDetail OrderDetail { get; set; }
}
And then bind to those fields i.e.
{Binding Customer.CustId}
Or if you don't want to change your bindings then pass the EF entities into the CustAndOrders object and in your properties just get and set from the entities:
public class CustAndOrders
{
public Customer Customer { get; set; }
public Order Order { get; set; }
public OrderDetail OrderDetail { get; set; }
// Customer table
public Int64 Cust_Id
{
get
{ return Customer.CustId;}
set
{ Customer.CustId = value; }
}
... Do this for the rest of your properties
And then query looks like this:
var query = from c in dc.Customers
join o in dc.Orders on c.CustId equals o.CustId
join od in dc.OrderDetails on o.OrderId equals od.OrderId
orderby o.OrderDate ascending, c.CustId, o.OrderAmount
select new CustAndOrders { Customer = c, Order = o, OrderDetail = od };
Given the following POCO Code First Entities
public class Customer
{
public int CustomerId { get; set; }
public string CustomerTitle { get; set; }
public string CustomerFirstName { get; set; }
public string CustomerLastName { get; set; }
public ICollection<Order> Orders { get; set; }
}
public class Order
{
public int OrderId { get; set; }
...
public int CustomerId { get; set; }
public Customer Customer { get; set; }
}
Using linq you can get the Orders filled in by using the Include property as in
var cust = (from cust in context.Customer
where cust.CustomerId == 1
select cust)
.Include(ord => ord.Orders)
.FirstOrDefault();
I am trying to get the same result using paramaterised sql, using
Customer co = context.Customer.SqlQuery(
#"select [Customer].[CustomerId],
...
[Order].[OrderId] AS [OrderId],
...
from Customer join Order on Customer.CustomerId = Order.CustomerId where Customer.CustomerId = #custid", sqlParm)
.FirstOrDefault();
How do I get the Orders in co.Orders to be populated with the above command, it seems like I can't use the Include statement with SqlQuery. This is a very simplified example for illustrative purposes only, the actual queries will be more involved.
This is not possible at all. Direct SQL execution doesn't offer filling of navigation properties and you really can't use Include. You must execute two separate SQL queries to get Cutomer and her Orders.
I have used the following class structure as workaround:
public class Customer
{
public int CustomerId { get; set; }
}
public class Order
{
public Customer Customer { get; set; }
private int _customerId;
private int CustomerId { get { return _customerId; } set { Customer.CustomerId = _customerId = value; } }
public Order()
{
Customer = new Customer();
}
}
In this case you don't need to run the query twice, and the following query would give the order with customer:
db.Database.SqlQuery<Order>(
"select cu.CustomerId, ord.OrderId from Orders ord join Customer cu on cu.CustomerId=ord.CustomerId")
.ToList();
What worked for me was to access the related member before the using is closed.
public static Customer GetCustomer (int custid)
{
Customer co = null;
using (var context = new YourEntities())
{
// your code
co = context.Customer.SqlQuery(
#"select [Customer].[CustomerId],
...
[Order].[OrderId] AS [OrderId],
...
from Customer join Order on Customer.CustomerId = Order.CustomerId where Customer.CustomerId = #custid", sqlParm)
.FirstOrDefault();
// my addition
// cause lazy loading of Orders before closing the using
ICollection<Order> orders = co.Orders;
}
// can access co.Orders after return.
return (co);
}