Given your class:
public class MergedList {
public int PrincipalId {get;set;}
public IEnumerable<int> CombinedIds {get;set;}
}
The simplest way to do this would probably be a Union if you only need unique values.
https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.union?view=net-8.0
var entitySource = _dBcontext.EntitySource.Select(e => new MergedList()
{
PrincipalId = e.Id,
CombinedIds = e.table1.Select(e => e.Id).Union(e.table2.Select(e => e.Id)).Union(e.table3.Select(e => e.Id))
}).FirstOrDefault();
Using my DbContext with code like this:
UnresolvedThreatAndCountermeasures =
product.ThreatAndCountermeasures.Where(tac => !tac.NotApplicable && !(tac.Verified && tac.Implemented)).Select(
vuln => new ProductSummaryVulnerabilityDtoHelper()
{
AllExternalLinkIds = vuln.ExternalLinks.Select(x => x.Id).Union(vuln.Threat.ExternalLinks.Select(x => x.Id)).Union(vuln.Threat.Fork.ExternalLinks.Select(x => x.Id))
})
SQL generated looks like this:
SELECT [t0].[Id], [t1].[Id] AS [Id0], [t2].[Id] AS [Id1], [t3].[Id] AS [Id2], [t0].[ProductId]
FROM [ThreatAndCountermeasure] AS [t0]
INNER JOIN [Threats] AS [t1] ON [t0].[ThreatId] = [t1].[Id]
LEFT JOIN [Threats] AS [t2] ON [t1].[ForkId] = [t2].[Id]
OUTER APPLY (
SELECT [e].[Id]
FROM [ExternalLink] AS [e]
WHERE [t0].[Id] = [e].[ThreatAndCountermeasureId]
UNION
SELECT [e0].[Id]
FROM [ExternalLink] AS [e0]
WHERE [t1].[Id] = [e0].[ThreatId]
UNION
SELECT [e1].[Id]
FROM [ExternalLink] AS [e1]
WHERE ([t2].[Id] IS NOT NULL) AND [t2].[Id] = [e1].[ThreatId]
) AS [t3]
WHERE [t0].[NotApplicable] = CAST(0 AS bit) AND ([t0].[Verified] = CAST(0 AS bit) OR [t0].[Implemented] = CAST(0 AS bit))
You could also do it like this if you for some reason actually want duplicates:
public class MergedList {
public int PrincipalId {get;set;}
public IEnumerable<int> CombinedIds {get;set;}
}
var entitySource = _dBcontext.EntitySource.Select(e => new MergedList()
{
PrincipalId = e.Id,
CombinedIds = CombineLists<int>(e.table1.Select(e => e.Id), e.table2.Select(e => e.Id), e.table3.Select(e => e.Id))
}).FirstOrDefault();
private static IEnumerable<T> CombineLists<T>(params IEnumerable<T>[] lists)
{
List<T> result = new();
foreach (var list in lists.Where(l => l != null))
{
result.AddRange(list);
}
return result;
}
I tested this using my DbContext like this:
UnresolvedThreatAndCountermeasures =
product.ThreatAndCountermeasures.Where(tac => !tac.NotApplicable && !(tac.Verified && tac.Implemented)).Select(
vuln => new ProductSummaryVulnerabilityDtoHelper()
{
AllExternalLinkIds = CombineLists<int>(vuln.ExternalLinks.Select(x => x.Id), vuln.Threat.ExternalLinks.Select(x => x.Id), vuln.Threat.Fork.ExternalLinks.Select(x => x.Id))
})
and this is the query generated with expected results:
SELECT [t0].[Id], [t1].[Id] AS [Id0], [t3].[Id] AS [Id1], [e].[Id] AS [Id2], [e0].[Id] AS [Id3], [e1].[Id] AS [Id4], [t0].[ProductId]
FROM [ThreatAndCountermeasure] AS [t0]
INNER JOIN [Threats] AS [t1] ON [t0].[ThreatId] = [t1].[Id]
LEFT JOIN [Threats] AS [t3] ON [t1].[ForkId] = [t3].[Id]
LEFT JOIN [ExternalLink] AS [e] ON [t0].[Id] = [e].[ThreatAndCountermeasureId]
LEFT JOIN [ExternalLink] AS [e0] ON [t1].[Id] = [e0].[ThreatId]
LEFT JOIN [ExternalLink] AS [e1] ON [t3].[Id] = [e1].[ThreatId]
WHERE [t0].[NotApplicable] = CAST(0 AS bit) AND ([t0].[Verified] = CAST(0 AS bit) OR [t0].[Implemented] = CAST(0 AS bit)
Using Concat like this:
var entitySource = _dBcontext.EntitySource.Select(e => new MergedList()
{
PrincipalId = e.Id,
CombinedIds = e.table1.Select(e => e.Id).Concat(e.table2.Select(e => e.Id)).Concat(e.table3.Select(e => e.Id))
}).FirstOrDefault();
Or like this:
CombinedValues = e.table1.Concat(e.table2).Concat(e.table3)
Would cause an exception like this for EF Core 7:
System.InvalidOperationException: 'Unable to translate a collection
subquery in a projection since either parent or the subquery doesn't
project necessary information required to uniquely identify it and
correctly generate results on the client side. This can happen when
trying to correlate on keyless entity type. This can also happen for
some cases of projection before 'Distinct' or some shapes of grouping
key in case of 'GroupBy'. These should either contain all key
properties of the entity that the operation is applied on, or only
contain simple property access expressions.'
Even if you try to project the list chances are quite high you would end up with something like this for Concat
:
System.InvalidOperationException: 'The LINQ expression 'MaterializeCollectionNavigation(
Navigation: MyObject.MyList,
subquery: DbSet<MyListObject>()
.Where(e => EF.Property<int?>(t.Outer.Outer, "Id") != null && object.Equals(
objA: (object)EF.Property<int?>(t.Outer.Outer, "Id"),
objB: (object)EF.Property<int?>(e, "MyObjectId")))
.Where(i => EF.Property<int?>(t.Outer.Outer, "Id") != null && object.Equals(
objA: (object)EF.Property<int?>(t.Outer.Outer, "Id"),
objB: (object)EF.Property<int?>(i, "MyObjectId"))))
.AsQueryable()
.Concat(MaterializeCollectionNavigation(
Navigation: MyObject2.MyList,
subquery: DbSet<MyListObject>()
.Where(e0 => EF.Property<int?>(t.Outer.Inner, "Id") != null && object.Equals(
objA: (object)EF.Property<int?>(t.Outer.Inner, "Id"),
objB: (object)EF.Property<int?>(e0, "MyObject2Id")))
.Where(i => EF.Property<int?>(t.Outer.Inner, "Id") != null && object.Equals(
objA: (object)EF.Property<int?>(t.Outer.Inner, "Id"),
objB: (object)EF.Property<int?>(i, "MyObject2Id")))))
.Concat(t.Outer.Inner.ForkId != null ? MaterializeCollectionNavigation(
Navigation: MyObject2.MyList,
subquery: DbSet<MyListObject>()
.Where(e1 => EF.Property<int?>(t.Inner, "Id") != null && object.Equals(
objA: (object)EF.Property<int?>(t.Inner, "Id"),
objB: (object)EF.Property<int?>(e1, "MyObject2Id")))
.Where(i => EF.Property<int?>(t.Inner, "Id") != null && object.Equals(
objA: (object)EF.Property<int?>(t.Inner, "Id"),
objB: (object)EF.Property<int?>(i, "MyObject2Id")))) : new List<MyListObject>())' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
See this post for more info:
https://github.com/dotnet/efcore/issues/26703#issuecomment-981843751
Where
should be before theSelect
. – Web