Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
pascal-910 opened this issue Jan 26, 2024 · 1 comment

Comments

@pascal-910
Copy link

pascal-910 commented Jan 26, 2024

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

@ajcvickers
Copy link
Member

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 ajcvickers changed the title AsNoTracking with IdentityResolution doesn't work if a reference navigation is projected Document that identity resolution does not apply to instances explicitly created with new Feb 1, 2024
@ajcvickers ajcvickers transferred this issue from dotnet/efcore Feb 1, 2024
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Feb 8, 2024
@ajcvickers ajcvickers reopened this Feb 8, 2024
@ajcvickers ajcvickers added this to the Backlog milestone Feb 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants