Skip to content

Incorrect NullReferenceException for parameter in split query with GroupBy #30022

@roji

Description

@roji

Reported by @excelkobayashi in #28940 (comment), with the following repro (thanks!):

Program.cs:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace QueryTest;

internal static class Program
{
	private static async Task Main()
	{
		using ServiceProvider services = Setup.CreateServices();

		using(IServiceScope seedScope = services.CreateScope())
		{
			MyContext context = seedScope.ServiceProvider.GetRequiredService<MyContext>();
			await Setup.Seed(context);
		}

		string userId = "1";
		string[] valueIds = new[] { "A", "B" };

		using(IServiceScope scope1 = services.CreateScope())
		{
			MyContext context = scope1.ServiceProvider.GetRequiredService<MyContext>();
			_ = await context.MyData.FindOtherUserValues1(userId, valueIds);
		}

		using(IServiceScope scope2 = services.CreateScope())
		{
			MyContext context = scope2.ServiceProvider.GetRequiredService<MyContext>();
			_ = await context.MyData.FindOtherUserValues2(userId, valueIds);
		}
	}
}

internal record MyData(string UserId, string? ValueId, int Id = 0) { }

internal static class Setup
{
	private static readonly IReadOnlyList<string> _seedUserIds = new string[] { "1", "2", "3" };
	private static readonly IReadOnlyList<string?> _seedValueIds = new string?[] { "A", "B", "C", null };

	public static ServiceProvider CreateServices()
	{
		return new ServiceCollection()
			.AddDbContext<MyContext>()
			.BuildServiceProvider(true);
	}

	public static async Task Seed(MyContext context)
	{
		_ = await context.Database.EnsureDeletedAsync();
		_ = await context.Database.EnsureCreatedAsync();

		foreach(string userId in _seedUserIds)
		{
			foreach(string? valueId in _seedValueIds)
			{
				MyData data = new(userId, valueId);
				_ = context.MyData.Add(data);
			}
		}

		_ = await context.SaveChangesAsync();
	}
}

internal class MyContext : DbContext
{
	required public DbSet<MyData> MyData { get; init; }

	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		_ = optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={nameof(QueryTest)};ConnectRetryCount=0",
			options => options.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
	}
}

internal static class Queries
{
	public static async Task<IReadOnlyDictionary<string, IReadOnlyList<string>>> FindOtherUserValues1(this IQueryable<MyData> source, string? excludeUserId, IEnumerable<string> includeValueIds, CancellationToken cancellationToken = default)
	{
		IQueryable<MyData> dataQuery = source
			.AsNoTracking()
			.Where(md => md.UserId != excludeUserId)
			.Where(md => includeValueIds.Contains(md.ValueId!));

		List<MyData> matches = await dataQuery.ToListAsync(cancellationToken);

		var groupOnClient = matches
			.GroupBy(md => md.ValueId!)
			.Select(grp => new { ValueId = grp.Key, UserIds = grp.Select(md => md.UserId!) });

		// No crash
		return groupOnClient.ToDictionary(grp => grp.ValueId, grp => (IReadOnlyList<string>)grp.UserIds.ToList());
	}

	public static async Task<IReadOnlyDictionary<string, IReadOnlyList<string>>> FindOtherUserValues2(this IQueryable<MyData> source, string? excludeUserId, IEnumerable<string> includeValueIds, CancellationToken cancellationToken = default)
	{
		IQueryable<MyData> dataQuery = source
			.AsNoTracking()
			.Where(md => md.UserId != excludeUserId)
			.Where(md => includeValueIds.Contains(md.ValueId!));

		var groupOnServer = dataQuery
			.GroupBy(md => md.ValueId!)
			.Select(grp => new { ValueId = grp.Key, UserIds = grp.Select(md => md.UserId!) });

		// Crashes here
		var groups = await groupOnServer.ToListAsync(cancellationToken);

		return groups.ToDictionary(grp => grp.ValueId, grp => (IReadOnlyList<string>)grp.UserIds.ToList());
	}
}

QueryTest.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.2" />
  </ItemGroup>

</Project>

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions