ho un problema con una query linq in cui provo a ottenere determinate informazioni da esso e semplicemente non riesco a trovare dove si trova il problema. Ho Strumenti, Attività e un'entità MN ToolTask nel mio db ef core. Questa query include 2 join esterni a sinistra e un gruppo alla fine.
Anche se sto controllando se i valori raggruppati non sono nulli, sto ancora ricevendo un messaggio di errore del tipo "L'oggetto nullibile deve avere un valore". Cosa mi sto perdendo qui?
Inoltre sembra che questa query linq sia valutata lato client, c'è qualcosa che posso fare per renderla lato server? È stato lato server quando non avevo la riga "let maxDateV ..." nel codice per qualche motivo.
var max = (from t in _fabContext.Tools
join tt in _fabContext.ToolTask on t.ToolId equals tt.ToolId into tt1
from tt in tt1.DefaultIfEmpty()
join ts in _fabContext.Tasks on tt.TaskId equals ts.TaskId into ts1
from ts in ts1.DefaultIfEmpty()
group tt by tt.ToolId into g
let maxOrderV = g.Max(c => c != null ? c.Order : 0)
let maxDateV = g.Max(c => c != null ? c.Task.ExportDate : DateTime.MinValue)
select new
{
ToolId = g.Key,
MaxOrder = maxOrderV,
MaxExportDate = maxDateV
}).ToDictionary(d => d.ToolId, d =>
new OrderExportDate {
Order = d.MaxOrder,
ExportDate = d.MaxExportDate
});
Aggiornamento 1 (classi di entità):
Compito
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace Main.DataLayer.EfClasses
{
public class Task
{
public int TaskId { get; private set; }
[Required]
public string Name { get; private set; }
[Required]
public int ProfileId { get; private set; }
[Required]
public DateTime ExportDate { get; private set; }
private HashSet<ToolTask> _toolTask;
public IEnumerable<ToolTask> ToolTask => _toolTask?.ToList();
private Task()
{
}
public Task(
string name,
int profileId,
DateTime exportDate)
{
Name = name;
ProfileId = profileId;
ExportDate = exportDate;
}
}
}
ToolTask
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace Main.DataLayer.EfClasses
{
public class ToolTask
{
public int ToolId
{
get; private set;
}
public Tool Tool
{
get; private set;
}
public int TaskId
{
get; private set;
}
public Task Task
{
get; private set;
}
[Required]
public int SortOrder
{
get; private set;
}
private ToolTask() { }
internal ToolTask(Tool tool, Task task, int sortOrder)
{
Tool = tool;
ToolId = tool.ToolId;
Task = task;
TaskId = task.TaskId;
SortOrder = sortOrder;
}
public void ChangeOrder(int order)
{
SortOrder = order;
}
}
}
Attrezzo
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Main.DataLayer.EfCode;
namespace Main.DataLayer.EfClasses
{
public class Tool
{
public int ToolId
{
get; private set;
}
[Required]
public string Name
{
get; private set;
}
[Required]
public string Model
{
get; private set;
}
private HashSet<ToolTask> _toolTask;
public IEnumerable<ToolTask> ToolTask => _toolTask?.ToList();
internal Tool() { }
public Tool(string name, string model)
{
Name = name;
Model = model;
_toolTask = new HashSet<ToolTask>();
}
public void AddTask(Task task, int sortOrder)
{
_toolTask?.Add(new ToolTask(this, task, sortOrder));
}
}
}
Ogni classe di entità ha una propria classe di configurazione, ma non c'è nulla di enorme al di fuori dell'impostazione delle chiavi primarie e della modalità di accesso per i campi di supporto.
Il problema con questa implementazione è che si raggruppa per tt.ToolId
, dove tt
proviene dal lato "destro" del join esterno sinistro, quindi tt
può essere null
.
Puoi facilmente risolverlo usando il campo corrispondente dal lato "sinistro", ad es. group tt by t.ToolId into g
.
Ma ciò non renderà la query completamente traducibile. Per fare ciò, è necessario (almeno con l'attuale traduttore di query EF Core) seguire alcune regole durante la creazione della query LINQ:
GroupBy
con aggregati, utilizzare sempre la parte {element}
del group {element} by {key}
costrutto group {element} by {key}
per preselezionare tutte le espressioni necessarie all'interno dei metodi aggregati. null
sono supportati naturalmente, anche quando il tipo di dati non è nullable e proviene dal lato facoltativo dei join. Ma non in C #, quindi devi specificare questo esplicito usando cast. Min
e Max
. Converti il risultato (non gli operandi) in valore sentinella, se necessario. Ma meglio tenerlo di tipo null
e nullable. Evita DateTime.MinValue
perché non ha rappresentazione in SqlServer. Applicandoli alla tua query:
var max =
(from t in _fabContext.Tools
from tt in t.ToolTask.DefaultIfEmpty()
let ts = tt.Task
group new
{
SortOrder = (int?)tt.SortOrder,
ExportDate = (DateTime?)ts.ExportDate
}
by new { t.ToolId }
into g
select new
{
ToolId = g.Key.ToolId,
MaxOrder = g.Max(e => e.SortOrder) ?? 0,
MaxExportDate = g.Max(e => e.ExportDate)
})
.ToDictionary(d => d.ToolId, d => new OrderExportDate
{
Order = d.MaxOrder,
ExportDate = d.MaxExportDate ?? DateTime.MinValue
});
che si traduce piacevolmente in una singola query SQL
SELECT [t].[ToolId], MAX([t.ToolTask].[SortOrder]) AS [MaxOrder], MAX([t.ToolTask.Task].[ExportDate]) AS [MaxExportDate]
FROM [Tool] AS [t]
LEFT JOIN [ToolTask] AS [t.ToolTask] ON [t].[ToolId] = [t.ToolTask].[ToolId]
LEFT JOIN [Task] AS [t.ToolTask.Task] ON [t.ToolTask].[TaskId] = [t.ToolTask.Task].[TaskId]
GROUP BY [t].[ToolId]
che è esattamente quello che le persone con background SQL si aspetterebbero.
Se non ti interessa la forma della query SQL tradotta, puoi evitare il raggruppamento e scrivere semplicemente la query LINQ in un modo logico semplice:
var max2 = _fabContext.Tools
.Select(t => new
{
t.ToolId,
MaxOrder = t.ToolTask.Max(tt => (int?)tt.SortOrder),
MaxExportDate = t.ToolTask.Max(tt => (DateTime?)tt.Task.ExportDate)
})
.ToDictionary(d => d.ToolId, d => new OrderExportDate
{
Order = d.MaxOrder ?? 0,
ExportDate = d.MaxExportDate ?? DateTime.MinValue
});