EntityFramework.Docs icon indicating copy to clipboard operation
EntityFramework.Docs copied to clipboard

Document that identity resolution does not apply to instances explicitly created with new

Open pascal-910 opened this issue 1 year ago • 1 comments

If I use AsNoTracking with IdentityResolution in combination with a projection in where a reference navigation property is also projected, then IdentityResolution doesn't work: Multiple principal instances will be created for entities having the same primary key. If the referenced navigation property is NOT projected it works as expected.

Example

In my example (full working UnitTest see below) I've got an 'OrderPosition' containing a reference navigation 'Article'. There're two order positions in database that each have a reference to the same (only) Article that exists.

This leads EFCore to create only one Article instance that is then referenced by both the positions (as expected):

var positions = ctx.OrderPosition.Select(
                op => new OrderPosition() {
                    OrderPositionId = op.OrderPositionId,
                    Article = op.Article
                })
                .AsNoTrackingWithIdentityResolution()
                .ToList();

But this leads EFCore to create TWO Article instances (what I think is wrong):

            var positions = ctx.OrderPosition.Select(
                op => new OrderPosition() {
                    OrderPositionId = op.OrderPositionId,
                    Article = new Article() {
                        ArticleId = op.ArticleId,
                        Description = op.Description
                    }
                })
                .AsNoTrackingWithIdentityResolution()
                .ToList();

The complete example (UnitTest)

using System.Linq;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Xunit;

namespace EFCore.Issue {
    class MyContext(DbContextOptions options) : DbContext(options) {
        public DbSet<OrderPosition> OrderPosition { get; set; }
        public DbSet<Article> Article { get; set; }
    }

    class OrderPosition {
        public int OrderPositionId { get; set; }
        public string? Description { get; set; }
        public int ArticleId { get; set; }
        public Article? Article { get; set; }
    }

    class Article {
        public int ArticleId { get; set; }
        public string? Description { get; set; }
    }


    public class TestIdentityResolution {
        private static DbContextOptions<T> CreateOptions<T>() where T : DbContext {
            var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" };
            var connectionString = connectionStringBuilder.ToString();
            var connection = new SqliteConnection(connectionString);
            if (connection.State != System.Data.ConnectionState.Open) {
                connection.Open();
            }
            var builder = new DbContextOptionsBuilder<T>();
            builder.UseSqlite(connection);
            return builder.Options;
        }


        static MyContext CreateContext() {
            var options = CreateOptions<MyContext>();
            FillData(options);
            return new MyContext(options);

            static void FillData(DbContextOptions<MyContext> options) {
                var dbContext = new MyContext(options);
                dbContext.Database.EnsureDeleted();
                dbContext.Database.EnsureCreated();
                dbContext.Article.Add(new Article { ArticleId = 99, Description = "Some article" });
                dbContext.OrderPosition.Add(new OrderPosition() {
                    OrderPositionId = 1,
                    Description = "Order pos 1",
                    ArticleId = 99
                });
                dbContext.OrderPosition.Add(new OrderPosition() {
                    OrderPositionId = 2,
                    Description = "Order pos 2",
                    ArticleId = 99
                });
                dbContext.SaveChanges();
            }
        }

        [Fact]
        public void Projection() {
            var ctx = CreateContext();
            var positions = ctx.OrderPosition.Select(
                op => new OrderPosition() {
                    OrderPositionId = op.OrderPositionId,
                    Article = op.Article
                })
                .AsNoTrackingWithIdentityResolution()
                .ToList();

            var pos1Art = positions[0].Article;
            var pos2Art = positions[1].Article;
            Assert.Equal(pos1Art, pos2Art);
        }

        [Fact]
        public void ProjectionAlsoReferenceProjected() {
            var ctx = CreateContext();
            var positions = ctx.OrderPosition.Select(
                op => new OrderPosition() {
                    OrderPositionId = op.OrderPositionId,
                    Article = new Article() {
                        ArticleId = op.ArticleId,
                        Description = op.Description
                    }
                })
                .AsNoTrackingWithIdentityResolution()
                .ToList();

            var pos1Art = positions[0].Article;
            var pos2Art = positions[1].Article;
            Assert.Equal(pos1Art, pos2Art);
        }
    }
}

ProjectFile

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

	<PropertyGroup>
		<TargetFramework>net8.0</TargetFramework>
		<Nullable>enable</Nullable>
		<WarningsAsErrors>Nullable</WarningsAsErrors>
		<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
	</PropertyGroup>

	<PropertyGroup>
		<TargetFramework>net8.0-windows</TargetFramework>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
		<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
		<PackageReference Include="Microsoft.NET.Test.Sdk"  Version="17.8.0"/>
		<PackageReference Include="xunit"  Version="2.6.3"/>
		<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5">
			<PrivateAssets>all</PrivateAssets>
			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
		</PackageReference>
	</ItemGroup>

</Project>

Used provider and versions

EF Core version: 8.0.1 Database provider: Microsoft.EntityFrameworkCore.Sqlite | Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 8.0 Operating system: Win 10.0.19044.3086] IDE: Visual Studio 2022 17.8.4

pascal-910 avatar Jan 26 '24 12:01 pascal-910

Note for triage: in this case the query is explicitly creating instances of the principal entity, which means that we don't do identity resolution here for tracking queries either.

ajcvickers avatar Jan 31 '24 12:01 ajcvickers