LINQ double INNER JOIN on query translation when using selectMany - c#

I have the following LINQ statement:
var repoActivityRowsTest = appManager.GetRepository<ActivityRow>();
var activityRowsTest = await repoActivityRowsTest.Search(f => f.ExcelReport.uploadPhase == RPToolConstants.Phase_Planning, includeProperties: "PlanningInfo")
.Where(f => iso3Alpha3List.Contains(f.ExcelReport.countryOfficeIso3Alpha3))
.SelectMany(sm => sm.PlanningInfo).Select(s => new { s.Year, s.Count, s.ActivityRow.UnitCost })
.GroupBy(g=>new { g.Year }).Select(sg=>new { sg.Key.Year, Total = sg.Sum(sum => sum.UnitCost * sum.Count) })
.ToListAsync();
Which uses the repository pattern. The search function is the one below:
public IQueryable<TEntity> Search(Expression<Func<TEntity, bool>> filter = null,
string includeProperties = "", bool trackChanges = false)
{
IQueryable<TEntity> query = context.Set<TEntity>();
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty.Trim());
}
if (!trackChanges)
{
query = query.AsNoTracking();
}
return query;
}
When I inspect the command that arrives in SQL Server I see that the query is translated in the following SQL:
SELECT [a0].[Year], SUM([a1].[UnitCost] * CAST([a0].[Count] AS decimal(18,2))) AS [Total]
FROM [ActivityRows] AS [a]
INNER JOIN [ExcelReports] AS [e] ON [a].[ExcelReportId] = [e].[Id]
INNER JOIN [ActivityRowPlanningInfo] AS [a0] ON [a].[Id] = [a0].[ActivityRowId]
INNER JOIN [ActivityRows] AS [a1] ON [a0].[ActivityRowId] = [a1].[Id]
WHERE ([e].[uploadPhase] = N'planning')
AND [e].[countryOfficeIso3Alpha3] IN (N'AFG', N'DZA', N'AGO', N'ARM', N'BGD')
GROUP BY [a0].[Year]
It works perfectly, but why there is an inner join duplicated:
INNER JOIN [ActivityRows] AS [a1] ON [a0].[ActivityRowId] = [a1].[Id]
is a non-sense to me!
If I remove it from the SQL it works as before. Is there any issue in my LINQ query that causes this strange SQL?
here is the definition of the entities:
public class ActivityRow : Entity<int>
{
public string Description { get; set; }
public int ExcelReportId { get; set; }
[ForeignKey("ExcelReportId")]
public virtual ExcelReport ExcelReport { get; set; }
public int ActivitySubTypeId { get; set; }
[ForeignKey("ActivitySubTypeId")]
public virtual ActivitySubType ActivitySubType { get; set; }
public int? ActivityCategoryId { get; set; }
[ForeignKey("ActivityCategoryId")]
public virtual ActivityCategory ActivityCategory { get; set; }
public string ResponsibleEntity { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal UnitCost { get; set; }
public string Notes { get; set; }
public virtual ICollection<ActivityRowReportingInfo> ReportingInfo { get; set; }
public virtual ICollection<ActivityRowPlanningInfo> PlanningInfo { get; set; }
}
public class ActivityRowPlanningInfo : Entity<int>
{
public int ActivityRowId { get; set; }
[ForeignKey("ActivityRowId")]
public virtual ActivityRow ActivityRow { get; set; }
public int Year { get; set; }
public int Quarter { get; set; }
public int Count { get; set; }
}
and here the definition of the relationships with fluent API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//activities
modelBuilder.Entity<ActivityRow>()
.HasMany(b => b.ReportingInfo)
.WithOne(t => t.ActivityRow)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<ActivityRow>()
.HasMany(b => b.PlanningInfo)
.WithOne(t => t.ActivityRow)
.OnDelete(DeleteBehavior.Cascade);
...etc.
}

Rewrite query via LINQ Query syntax and you can simplify your query with ease.
The following query do not create non wanted joins:
var repoActivityRowsTest = appManager.GetRepository<ActivityRow>();
var activityRows = repoActivityRowsTest
.Search(f => true);
var resultQuery =
from ar in activityRows
where
ar.ExcelReport.uploadPhase == RPToolConstants.Phase_Planning
&& iso3Alpha3List.Contains(ar.ExcelReport.countryOfficeIso3Alpha3)
from pi in ar.PlanningInfo
group new { ar, pi } by new { pi.Year } into g
select new
{
g.Key.Year,
Total = g.Sum(x => x.ar.UnitCost * x.pi.Count)
};
var result = await resultQuery.ToListAsync();

Related

EF Core union with value conversion

I want my union LINQ query to be evaluated on server side with EF Core.
There're entities:
public class Entity1
{
public int Id { get; set; }
public List<StringWithStyle> Names { get; set; } = new List<StringWithStyle>();
}
public class Entity2
{
public int Id { get; set; }
public StringWithStyle Name { get; set; }
}
public class StringWithStyle
{
public string Text { get; set; }
public bool IsBold { get; set; }
public bool IsItalic { get; set; }
public bool IsUpperCase { get; set; }
}
Their properties are stored in DbContext as json string using Value conversion:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Entity1>()
.HasKey(e => e.Id);
modelBuilder.Entity<Entity1>()
.Property(e => e.Names)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<StringWithStyle>>(v, (JsonSerializerOptions)null)
,
new ValueComparer<List<StringWithStyle>>(
(arr1, arr2) => arr1.Count() == arr2.Count() && !arr1.Except(arr2).Any(),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())))
);
modelBuilder.Entity<Entity2>()
.HasKey(e => e.Id);
modelBuilder.Entity<Entity2>()
.Property(e => e.Name)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<StringWithStyle>(v, (JsonSerializerOptions)null)
,
new ValueComparer<StringWithStyle>(
(val1, val2) => val1.Equals(val2),
c => c.GetHashCode())
);
}
I need to show both entities in one grid. So, I use such a query:
var entities1 = from e1 in dbContext.Set<Entity1>()
select new GridModel
{
Id = e1.Id,
IsFirst = true,
Names = e1.Names,
Name = default
};
var entities2 = from e2 in dbContext.Set<Entity2>()
select new GridModel
{
Id = e2.Id,
IsFirst = false,
Name = e2.Name,
Names = default
};
var grid = entities1.Union(entities2).ToList();
And it throws an Exception:
System.InvalidOperationException : Unable to translate set operation after client projection has been applied. Consider moving the set operation before the last 'Select' call.
Is it possible to to get such a query that is evaluating on server side?
*** UPDATE ***
There's GridModel class:
public class GridModel
{
public int Id { get; set; }
public bool IsFirst { get; set; }
public List<StringWithStyle> Names { get; set; }
public StringWithStyle Name { get; set; }
}

C# web api data filtration

Good day,
I am doing Web api rest project and want to include product search for products by size and color, but I want to be able search for example:
1 One size
[httpGet][Route("oneSize/{sizeID}")]
2 Two Sizes
[httpGet][Route("TwoSizes/{sizeID1}/{sizeID2}")]
3 One size/ One color
[httpGet][Route("OneSizeOneColor/{sizeID1}/{ColorID}")]
4 Two sizes/ One color
[httpGet][Route("TwoSizeOneColor/{sizeID1}/{sizeID2}/{ColorID}")]
etc.
Do I need to create end point for every tipe of search or is there a smarter way of doing it?
You should use the query params. You can add them via FromQuery attribute:
[HttpGet]
public IActionResult SearchProducts([FromQuery] int[] sizeIds, [FromQuery] int[] colorIds) {
}
You can replace int with string if you have string Ids.
For example, if you want to make a request with sizes 1 and 2, and color 3 and 4, the request would look like this: https://localhost:5001/your-endpoint-name?sizeIds=1&sizeIds=2&colorIds=3&colorIds=4
So query is a list of key=value url parameters after the ? separated by & sign
EDIT
You can easily query the database with the sql IN operator.
In EF Core, it would look something like this:
IQuaryable<Product> query = dbContext.Products;
if (sizeIds.Length > 0) {
query= query.Where(p => sizeIds.Contains(p.SizeId));
}
if (colorIds.Length > 0) {
query= query.Where(p => colorIds.Contains(p.ColorId));
}
List<Product> result = await query.ToListAsync();
It would be translated to the following SQL:
SELECT * FROM Products
WHERE Products.SizeId IN (1, 2) AND Products.ColorId IN (3, 4);
The problem is that I have nested classess
public class ProductBase
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<ProductVariant> Variants { get; set; } = new List<ProductVariant>();
public int BaseImageId { get; set; } = 0;
public string BaseImagePath { get; set; } = string.Empty;
}
public class ProductVariant
{
public int Id { get; set; }
public int Quantity { get; set; }
public int ProductBaseId { get; set; }
public int ProductSizeId { get; set; }
public ProductSize ProductSize { get; set; }
public int ProductColorId { get; set; }
public ProductColor ProductColor { get; set; }
public IEnumerable<ImageVariant> imageVariants { get; set; } = new List<ImageVariant>();
}
public class ProductSize
{
public int Id { get; set; }
public string Size { get; set; }
}
public class ProductColor
{
public int Id { get; set; }
public string Color { get; set; }
}
I am trying something like this
public async Task<IQueryable<Models.ProductBase>> SearchProducts(int[] SizeIds, int[] ColorIds )
{
IQueryable<Models.ProductBase> query = _dataContext.ProductBases
.Include(pb => pb.Variants).ThenInclude(v => v.ProductSize)
.Include(pb => pb.Variants).ThenInclude(v => v.ProductColor)
.Include(pb => pb.Variants).ThenInclude(v => v.imageVariants);
if(SizeIds.Length > 0)
{
query = query.Where(pb => SizeIds.Contains(pb.Variants.Any(pb.Variants.doesnotWork));
}
if(ColorIds.Length > 0)
{
query = query.Where(pb => ColorIds.Contains(pb.Variants.Contains(pb.Variants.doesNotWork)));
}
List<ProductBase> result = await query.ToListAsync();
}
Fixed , this is working now
public async Task<IEnumerable<Models.ProductBase>> SearchProducts(int[] SizeIds, int[] ColorIds )
{
IQueryable<Models.ProductBase> query = _dataContext.ProductBases
.Include(pb => pb.Variants).ThenInclude(v => v.ProductSize)
.Include(pb => pb.Variants).ThenInclude(v => v.ProductColor)
.Include(pb => pb.Variants).ThenInclude(v => v.imageVariants);
if(SizeIds.Length > 0)
{
query = query.Where(pb => pb.Variants.ToList().Any(v => SizeIds.Contains(v.ProductSizeId)));
}
if(ColorIds.Length > 0)
{
query = query.Where(pb => pb.Variants.ToList().Any(v => ColorIds.Contains(v.ProductColorId)));
}
List<Models.ProductBase> result = await query.ToListAsync();
return result;
}

Applying a Linq to Entities join on my code

I have code that works, but I worked around a 'Join' in Linq to Entities, because I could not figure it out.
Could you please show me how to succesfully apply it to my code?
My desired result is a dictionary:
Dictionary<string, SelectedCorffData> dataSelectedForDeletion = new Dictionary<string, SelectedCorffData>();
The above mentioned class:
public class SelectedCorffData
{
public long CorffId { get; set; }
public string ReportNumber { get; set; }
public DateTime CorffSubmittedDateTime { get; set; }
}
Please note the 'intersectResult' I am looping through is just a string collection.
Here is my code:
DateTime dateToCompare = DateTime.Now.Date;
Dictionary<string, SelectedCorffData> dataSelectedForDeletion = new Dictionary<string, SelectedCorffData>();
foreach (var mafId in intersectResult)
{
var corffIdsPerMaf = context
.Mafs
.Where(m => m.MafId == mafId)
.Select(m => m.CorffId);
var corffIdForMaf = context
.Corffs
.Where(c => corffIdsPerMaf.Contains(c.Id))
.OrderByDescending(c => c.CorffSubmittedDateTime)
.Select(c => c.Id)
.First();
//Selected close-out forms, whose MAF's may be up for deletion, based on date.
var corffData = context
.Corffs
.Where(c => c.Id == corffIdForMaf && System.Data.Entity.DbFunctions.AddYears(c.CorffSubmittedDateTime, 1).Value > dateToCompare)
.Select(c => new SelectedCorffData () { CorffId = c.Id, ReportNumber = c.ReportNumber, CorffSubmittedDateTime = c.CorffSubmittedDateTime })
.FirstOrDefault();
if(corffData != null)
{
dataSelectedForDeletion.Add(mafId, corffData);
}
}
Please note: this is not just a simple join. If it can't be simplified, please tell me. Also please explain why.
The code below I don't think is exactly right but it is close to what you need. I simulated the database so I could get the syntax correct.
namespace System
{
namespace Data
{
namespace Entity
{
public class DbFunctions
{
public static Data AddYears(DateTime submittedTime, int i)
{
return new Data();
}
public class Data
{
public int Value { get; set; }
}
}
}
}
}
namespace ConsoleApplication23
{
class Program
{
static void Main(string[] args)
{
Context context = new Context();
int dateToCompare = DateTime.Now.Year;
var corffIdsPerMaf = context.Mafs.Select(m => new { id = m.CorffId, mafs = m}).ToList();
var corffIdForMaf = context.Corffs
.Where(c => System.Data.Entity.DbFunctions.AddYears(c.CorffSubmittedDateTime, 1).Value > dateToCompare)
.OrderByDescending(c => c.CorffSubmittedDateTime).Select(c => new { id = c.Id, corff = c}).ToList();
var intersectResult = from p in corffIdsPerMaf
join f in corffIdForMaf on p.id equals f.id
select new SelectedCorffData() { CorffId = p.id, ReportNumber = f.corff.ReportNumber, CorffSubmittedDateTime = f.corff.CorffSubmittedDateTime };
Dictionary<string, SelectedCorffData> dataSelectedForDeletion = intersectResult.GroupBy(x => x.ReportNumber, y => y).ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
}
public class Context
{
public List<cMafs> Mafs { get; set;}
public List<cCorffs> Corffs { get; set;}
}
public class cMafs
{
public int CorffId { get; set; }
}
public class cCorffs
{
public DateTime CorffSubmittedDateTime { get; set; }
public int Id { get; set; }
public string ReportNumber { get; set; }
}
public class Test
{
}
public class SelectedCorffData
{
public long CorffId { get; set; }
public string ReportNumber { get; set; }
public DateTime CorffSubmittedDateTime { get; set; }
}
}

How to join two LINQ queries

I have two related entities with composite keys:
public class Event
{
public string ID1 { get; set; }
public int ID2 { get; set; }
public DateTime EventDate { get; set; }
public string EventData { get; set; }
public string DocID1 { get; set; }
public int DocID2 { get; set; }
}
public class EventDocument
{
public string ID1 { get; set; }
public int ID2 { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Number { get; set; }
public virtual ICollection<Event> Events { get; set; }
}
Is there a possibility to filter first both of them by some criteria and then to join results because of big amount of records?
Acctually I can reach related Events when I filter EventDocuments, but I also need a possibility to filter Event and EventDocument in one time.
I am trying to do like this:
var events = ModelContext.Events.AsNoTracking().Select(x => x);
events = events.Where(x => x.EventData.StartsWith(FilterCriteria));
var eventDocuments = ModelContext.EventDocuments.AsNoTracking().Select(x => x);
eventsDocuments = eventDocuments.Where(x => x.LastName.StartsWith(FilterLastName));
And now I need to join these to queries and get a result - filtered and joined data from two entities
Trying to do like this:
var result = eventDocuments.Join(events,
doc => new { doc.ID1, doc.ID2 },
ev => new { cross.DocID1, cross.DocID2},
(doc, ev) => new { EventDocument = doc, Event = ev });
You can simply query both sets with SelectMany. In query syntax this would look like:
var eventsQry =
from eventDocument in eventDocuments
where eventDocument.LastName.StartsWith(FilterLastName)
from ev in events
where ev.EventData.StartsWith(FilterCriteria) && (ev.ID1 == eventDocument.ID1) && (ev.ID2 == eventDocument.ID2)
select new { eventDocument, ev };
You don't need to use one query to filter your results. You can combine multiple queries:
var eventsQry =
from ev in events
where ev.EventData.StartsWith(FilterCriteria)
select ev
var documentsQry =
from eventDocument in documentsQry
where eventDocument.LastName.StartsWith(FilterLastName)
select eventDocument;
var combinedQry =
from eventDocument in documentsQry
from ev in eventsQry
where (ev.ID1 == eventDocument.ID1) && (ev.ID2 == eventDocument.ID2)
select new { eventDocument, ev };

How to distinct by multiple columns with input parameter in Linq

i have tre tables T020_CLIENTI,T021_SITI,T520_REL_STRUMENTI_SITI that i would join and then distinct by T020.Ragione_sociale,T520.DA_DATA,T520.A_DATA but obtain as return parameters T020.Ragione_sociale,T020.id_cliente,T520.cod_stumento,T520.DA_DATA,T520.A_DATA
my tables are
public partial class T020_CLIENTI
{
public decimal ID_CLIENTE { get; set; }
public Nullable<decimal> ID_COMUNE { get; set; }
public Nullable<decimal> ID_CONSORZIO { get; set; }
public string COD_LINEA_ATTIVITA { get; set; }
}
public partial class T021_SITI
{
public decimal ID_SITO { get; set; }
public Nullable<decimal> ID_FORNITORE { get; set; }
public Nullable<decimal> ID_CLIENTE { get; set; }
}
public partial class T520_REL_STRUMENTI_SITI
{
public string COD_STUMENTO { get; set; }
public decimal ID_SITO { get; set; }
public System.DateTime DA_DATA { get; set; }
public System.DateTime A_DATA { get; set; }
}
my linq query is
using (var cont = DALProvider.CreateEntityContext())
{
var query =
from cliente in cont.T020_CLIENTI
from sito
in cont.T021_SITI
.Where(s => s.ID_CLIENTE == cliente.ID_CLIENTE)
.DefaultIfEmpty()
from relStrumenti
in cont.T520_REL_STRUMENTI_SITI
.Where(s => s.ID_SITO == sito.ID_SITO)
.DefaultIfEmpty()
select new
{
clienteRec = cliente,
sitoRec = sito,
relStrumentiRec = relStrumenti
};
if (!string.IsNullOrEmpty(aiFiltro.RAGIONE_SOCIALE))
query = query.Where(i => i.clienteRec.RAGIONE_SOCIALE.ToUpper().Contains(aiFiltro.RAGIONE_SOCIALE.ToUpper()));
var vRes = (from clienteDef in query
select new ClienteFiltrato
{
RAGIONE_SOCIALE = clienteDef.clienteRec.RAGIONE_SOCIALE,
ID_CLIENTE = clienteDef.clienteRec.ID_CLIENTE,
COD_STRUMENTO = clienteDef.relStrumentiRec.COD_STUMENTO,
DATA_DA = clienteDef.relStrumentiRec.DA_DATA,
DATA_A = clienteDef.relStrumentiRec.A_DATA
}) ;
return vRes.AsQueryable();
}
but in my linq query i don't know where i can insert distinct and input parameter (:pPOD) to obtain my linq that in oracle query is:
SELECT DISTINCT t020.ragione_sociale,
da_data,
a_data,
t020.id_Cliente,
:pPOD
FROM t020_clienti t020, t021_siti t021, T520_REL_STRUMENTI_SITI t520
WHERE t020.id_cliente = t021.id_cliente
AND t021.id_sito = t520.id_sito
AND (:pPOD is null or t520.cod_stumento = :pPOD)
ORDER BY da_data
where :pPOD is an input parameter that i could have set or not.
Try to add (s.COD_STUMENTO == pPod || pPod == null) to your Where clause, where you are filtering T520_REL_STRUMENTI_SITI entity. pPod should be a string variable.
Please have in mind that if you are using DefaultIfEmpty() in LINQ this will be translated to left join in SQL.
Modified query follows:
string pPod = null;
using (var cont = DALProvider.CreateEntityContext())
{
var query =
(from cliente in cont.T020_CLIENTI
from sito
in cont.T021_SITI
.Where(s => s.ID_CLIENTE == cliente.ID_CLIENTE)
.DefaultIfEmpty()
from relStrumenti
in cont.T520_REL_STRUMENTI_SITI
.Where(s => s.ID_SITO == sito.ID_SITO && (s.COD_STUMENTO == pPod || pPod == null))
.DefaultIfEmpty()
select new
{
clienteRec = cliente.Distinct(),
sitoRec = sito,
relStrumentiRec = relStrumenti
});
if (!string.IsNullOrEmpty(aiFiltro.RAGIONE_SOCIALE))
query = query.Where(i => i.clienteRec.RAGIONE_SOCIALE.ToUpper().Contains(aiFiltro.RAGIONE_SOCIALE.ToUpper()));
var vRes = (from clienteDef in query
select new ClienteFiltrato
{
RAGIONE_SOCIALE = clienteDef.clienteRec.RAGIONE_SOCIALE,
ID_CLIENTE = clienteDef.clienteRec.ID_CLIENTE,
COD_STRUMENTO = clienteDef.relStrumentiRec.COD_STUMENTO,
DATA_DA = clienteDef.relStrumentiRec.DA_DATA,
DATA_A = clienteDef.relStrumentiRec.A_DATA
}).Distinct() ;
return vRes.AsQueryable();
}
You can use:
string query =
((System.Data.Objects.ObjectQuery)query).ToTraceString();
This will show you the generated SQL from LINQ Queryable object.

Categories