efcore icon indicating copy to clipboard operation
efcore copied to clipboard

EF Core: two joins to the same table via Fluent API

Open verdysh opened this issue 3 years ago • 7 comments

Hi!

EntityFrameworkCore 6.0.5

I have such tables in DB:

ApplicationsControlMonitorEntriesAggregate
	...
	TenantId
	ApplicationsControlAggregateId
	...

ApplicationsControlAggregate
	...
	string ProcessCaption
	...
	
ProcessStatisticRule
	...
	string ProcessName
	int? TenantId
	...

ApplicationsControlAggregateId is ForeignKey on appropriate table.

ProcessStatisticRule has not any physical relations with any tables. But it has logical relation with ApplicationsControlAggregate by ProcessName.

SQL query I want to create with EF:

select *
from
	ApplicationsControlMonitorEntriesAggregate ApplicationsControlMonitorEntriesAggregate
	
	join ApplicationsControlAggregate ApplicationsControlAggregate
		on ApplicationsControlMonitorEntriesAggregate.ApplicationsControlAggregateId = ApplicationsControlAggregate.Id
	
	join ProcessStatisticRule ProcessStatisticRule1
		on ApplicationsControlAggregate.ProcessCaption = ProcessStatisticRule1.ProcessName
			and ProcessStatisticRule1.TenantId is null
	
	join ProcessStatisticRule ProcessStatisticRule2
		on ApplicationsControlAggregate.ProcessCaption = ProcessStatisticRule2.ProcessName
			and ProcessStatisticRule2.TenantId = ApplicationsControlMonitorEntriesAggregate.TenantId

Here is my query with EF:

var query = db
	.ApplicationsControlMonitorEntriesAggregates.Where(a => a.TenantId == _tenantId)
	.Join(
		db.ProcessStatisticRules,
		a => new ProcessStatisticRulesKey(a.ApplicationsControlAggregates.ProcessCaption, null),
		psr => new ProcessStatisticRulesKey(psr.ProcessName, psr.TenantId),
		(a, psr) => new ApplicationRiskTemporarySelector(a, psr, null)
	)
	.Join(
		db.ProcessStatisticRules,
		a => new ProcessStatisticRulesKey(a.ApplicationsControlMonitorEntriesAggregate.ApplicationsControlAggregates.ProcessCaption, a.ApplicationsControlMonitorEntriesAggregate.TenantId),
		psr => new ProcessStatisticRulesKey(psr.ProcessName, psr.TenantId),
		(a, psr) => new ApplicationRiskTemporarySelector(a.ApplicationsControlMonitorEntriesAggregate, a.ProcessStatisticRuleGeneral, psr)
	)
	.Select(r => new ApplicationRiskRow
	{
		FilesCount = r.ApplicationsControlMonitorEntriesAggregate.FilesCount
	})	
;

It fails with error:

The LINQ expression 'DbSet<ApplicationsControlMonitorEntriesAggregate>()\r\n .Where(a => a.TenantId == ___tenantId_0)\r\n .Join(\r\n inner: DbSet<ApplicationsControlAggregate>(), \r\n outerKeySelector: a => EF.Property<int?>(a, "ApplicationsControlAggregatesId"), \r\n innerKeySelector: a0 => EF.Property<int?>(a0, "Id"), \r\n resultSelector: (o, i) => new TransparentIdentifier<ApplicationsControlMonitorEntriesAggregate, ApplicationsControlAggregate>(\r\n Outer = o, \r\n Inner = i\r\n ))\r\n .Join(\r\n inner: DbSet<ProcessStatisticRule>(), \r\n outerKeySelector: a => new ProcessStatisticRulesKey(\r\n a.Inner.ProcessCaption, \r\n null\r\n ), \r\n innerKeySelector: p => new ProcessStatisticRulesKey(\r\n p.ProcessName, \r\n p.TenantId\r\n ), \r\n resultSelector: (a, p) => new TransparentIdentifier<TransparentIdentifier<ApplicationsControlMonitorEntriesAggregate, ApplicationsControlAggregate>, ProcessStatisticRule>(\r\n Outer = a, \r\n Inner = p\r\n ))\r\n .Join(\r\n inner: DbSet<ProcessStatisticRule>(), \r\n outerKeySelector: ti => new ProcessStatisticRulesKey(\r\n new ApplicationRiskTemporarySelector(\r\n ti.Outer.Outer, \r\n ti.Inner, \r\n null\r\n ).ApplicationsControlMonitorEntriesAggregate.ApplicationsControlAggregates.ProcessCaption, \r\n (int?)new ApplicationRiskTemporarySelector(\r\n ti.Outer.Outer, \r\n ti.Inner, \r\n null\r\n ).ApplicationsControlMonitorEntriesAggregate.TenantId\r\n ), \r\n innerKeySelector: p0 => new ProcessStatisticRulesKey(\r\n p0.ProcessName, \r\n p0.TenantId\r\n ), \r\n resultSelector: (ti, p0) => new TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<ApplicationsControlMonitorEntriesAggregate, ApplicationsControlAggregate>, ProcessStatisticRule>, ProcessStatisticRule>(\r\n Outer = ti, \r\n Inner = p0\r\n ))' 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.

If I remove second one Join, for example:

var query = db
	.ApplicationsControlMonitorEntriesAggregates.Where(a => a.TenantId == _tenantId)
	.Join(
		db.ProcessStatisticRules,
		a => new ProcessStatisticRulesKey(a.ApplicationsControlAggregates.ProcessCaption, null),
		psr => new ProcessStatisticRulesKey(psr.ProcessName, psr.TenantId),
		(a, psr) => new ApplicationRiskTemporarySelector(a, psr, null)
	)
	.Select(r => new ApplicationRiskRow
	{
		FilesCount = r.ApplicationsControlMonitorEntriesAggregate.FilesCount
	})	
;

It works.

How can I do these two joins via Fluent API?

verdysh avatar Aug 04 '22 10:08 verdysh

Don't use custom types for join keys.

smitpatel avatar Aug 04 '22 16:08 smitpatel

Initially I tried with anonymous types. But in second Join compiler says that it can't infer type and I can't specify needed generic type in this case because that is anonymous type. Or I can?

After that I declared usual types and tried to use them. I found out that in this case you can't use them using type initializer. Like this: new ProcessStatisticRulesKey{a = a.ApplicationsControlAggregates.ProcessCaption, b = null} In this case you will face an error during runtime. Error says something similar to what I get now, something about "could not be translated" and all that stuff.

The only way I found to get rid of this error - this is creation constructor for type and use this constructor in expression. It works if you have only one join to particular table. But when you add one more join to the same table, you face error I described in start post.

So, what I should use in this case?

verdysh avatar Aug 04 '22 17:08 verdysh

But in second Join compiler says that it can't infer type and I can't specify needed generic type in this case because that is anonymous type. Or I can?

That should work as long as you make sure both anonymous types are same shape. Compiler will generate same type for it.

smitpatel avatar Aug 04 '22 19:08 smitpatel

@smitpatel , thanks for the answer!

I got almost I want.

var query = db
	.ApplicationsControlMonitorEntriesAggregates.Where(a => a.TenantId == _tenantId)
	.Join(
		db.ProcessStatisticRules,
		a => new
		{
			ProcessCaption = a.ApplicationsControlAggregates.ProcessCaption,
			TenantId = (int?) null
		},
		psr => new
		{
			ProcessCaption = psr.ProcessName,
			TenantId = (int?) psr.TenantId
		},
		(a, psr) => new
		{
			ApplicationsControlMonitorEntriesAggregate = a,
			ProcessStatisticRuleGeneral = psr
		}
	)
	.Join(
		db.ProcessStatisticRules,
		a => new
		{
			ProcessCaption = a.ApplicationsControlMonitorEntriesAggregate.ApplicationsControlAggregates.ProcessCaption,
			TenantId = (int?)_tenantId
		},
		(psr) => new
		{
			ProcessCaption = psr.ProcessName,
			TenantId = (int?)psr.TenantId
		},
		(a, psr) => new
		{
			ApplicationsControlMonitorEntriesAggregate = a.ApplicationsControlMonitorEntriesAggregate,
			ProcessStatisticRuleGeneral = a.ProcessStatisticRuleGeneral,
			ProcessStatisticRuleByTenant = psr
		}
	)
	.Select(r => new ApplicationRiskRow
	{
		FilesCount = r.ApplicationsControlMonitorEntriesAggregate.FilesCount,
	})
;

I said "almost" because this code turns into the next SQL query:

SELECT [a].[FilesCount]
FROM [ApplicationsControlMonitorEntriesAggregates] AS [a]
INNER JOIN [ApplicationsControlAggregates] AS [a0] ON [a].[ApplicationsControlAggregatesId] = [a0].[Id]
INNER JOIN [ProcessStatisticRules] AS [p] ON ([a0].[ProcessCaption] = [p].[ProcessName]) AND ([p].[TenantId] IS NULL)
INNER JOIN [ProcessStatisticRules] AS [p0] ON ([a0].[ProcessCaption] = [p0].[ProcessName]) AND (@___tenantId_1 = [p0].[TenantId])
WHERE [a].[TenantId] = @___tenantId_0

But I need LEFT JOIN instead of INNER JOIN. I was trying to put DefaultOfEmty() wherever compiler allowed it, but in all cases I got not the query I really want. For example if I add DefaultOrEmpty() to first Join:

var query = db
	.ApplicationsControlMonitorEntriesAggregates.Where(a => a.TenantId == _tenantId)
	.Join(...).DefaultOrEmpty()
	.Join(...)
	.Select()

I get the next SQL query:

SELECT [t].[FilesCount]
FROM (
  SELECT NULL AS [empty]
) AS [e]
LEFT JOIN (
  SELECT [a].[FilesCount], [a0].[ProcessCaption]
  FROM [ApplicationsControlMonitorEntriesAggregates] AS [a]
  INNER JOIN [ApplicationsControlAggregates] AS [a0] ON [a].[ApplicationsControlAggregatesId] = [a0].[Id]
  INNER JOIN [ProcessStatisticRules] AS [p] ON ([a0].[ProcessCaption] = [p].[ProcessName]) AND ([p].[TenantId] IS NULL)
  WHERE [a].[TenantId] = @___tenantId_0
) AS [t] ON 1 = 1
INNER JOIN [ProcessStatisticRules] AS [p0] ON ([t].[ProcessCaption] = [p0].[ProcessName]) AND (@___tenantId_1 = [p0].[TenantId])

How can I get just LEFT JOINs in my case?

verdysh avatar Aug 04 '22 21:08 verdysh

See https://docs.microsoft.com/en-us/ef/core/querying/complex-query-operators#left-join

smitpatel avatar Aug 04 '22 22:08 smitpatel

Unfortunately, there is no example for Fluent API.

verdysh avatar Aug 05 '22 07:08 verdysh

It seems I've done it! For LEFT JOIN you must use SelectMany() instead Join().

Am I the only one who thinks this is a good example of a bad API? It's so not obvious and obscure.

Somebody knows why we still don't have methods like LeftJoin(), RightJoin(), FullJoin()?

verdysh avatar Aug 05 '22 08:08 verdysh