efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Incorrect translation of comparison of current value with owned type default value

Open d-kistanov-parc opened this issue 2 years ago • 2 comments

We have:

  • EF EntityFrameworkCore (7.0.5)
  • Database provider: Microsoft.EntityFrameworkCore.SqlServer
  • Target framework: .NET 7.0
  • Automapper last version (12.0.1)

Test case: Entity with Owned property. Map to DTO via "ProjectTo".

The main problem is that the mapping works only in the situation when all the properties are filled in, otherwise the owned property is not mapped. In my opinion, it happens due to the fact that the expression translation (where the comparison with the empty model is done) checks if all properties of the owned model are filled in instead of checking the filling in of one property, at least.

My example: https://github.com/d-kistanov-parc/EF_Automapper_Error_OwnType/blob/master/ConsoleApp/Program.cs

In other words, when i use ProjectTo method, the following Expression is formed:

db.Requests.ProjectTo<RequestDto>(config).Expression	
{
	[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression]
	.Select(dtoRequest => new RequestDto() 
	{
		Id = dtoRequest.Id, 
		Info = IIF((dtoRequest.Info == default(Object)), null, new RequestInfoDto() 
		{
			Text = dtoRequest.Info.Text, 
			Number = dtoRequest.Info.Number
		})
	})
}	System.Linq.Expressions.Expression {System.Linq.Expressions.MethodCallExpression2}

EntityFramework translates it into:

SELECT 
	[r].[Id], 
	CASE
		WHEN([r].[Info_Number] IS NULL) OR ([r].[Info_Text] IS NULL) THEN CAST(1 AS bit)
		ELSE CAST(0 AS bit)
	END,
	[r].[Info_Text],
	[r].[Info_Number]
FROM[Requests] AS[r]

I have got a question. Why is there "OR" operator, not "AND" ?

d-kistanov-parc avatar May 30 '23 10:05 d-kistanov-parc

I can confirm I have the exact same issue and as you suggested when I filled all the properties it returned the object instead of null. Is there a workaround until this is fixed?

georgyevv avatar Jul 10 '23 16:07 georgyevv

I found a workaround that seems to work for me. If we set DoNotAllowNull in AutoMapper config it will always create the new object no matter the values of the properties. Here is what I did:

In AutoMapper config add this

CreateMap<Location, LocationResponse>()
                .ForMember(dest => dest.Address, opt => opt.DoNotAllowNull())

This changes the Expression that AutoMapper generates to this

[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression]
    .AsNoTracking()
    .Where(x => !x.IsDeleted && (x.BusinessId == AuthService.GetBusinessIdFromRequestDomain()))
    .Select(
        dtoLocation => new LocationResponse
        {
            Name = dtoLocation.Name,
            Address = new AddressDetails
                {
                    Country = dtoLocation.Address.Country,
                    City = dtoLocation.Address.City,
                    Postcode = dtoLocation.Address.Postcode,
                    District = dtoLocation.Address.District,
                    Street = dtoLocation.Address.Street,
                    StreetNumber = dtoLocation.Address.StreetNumber,
                    Building = dtoLocation.Address.Building,
                    Entrance = dtoLocation.Address.Entrance,
                    Floor = dtoLocation.Address.Floor,
                    Apartment = dtoLocation.Address.Apartment,
                    AdditionalInformation = dtoLocation.Address.AdditionalInformation
                }
        })

georgyevv avatar Jul 11 '23 09:07 georgyevv

Note for triage: this is a regression from 6.0. Repro without auto-mapper:

#nullable disable

using (var context = new SomeDbContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.AddRange(
        new Request { Id = 1 },
        new Request { Id = 2, Info = new() },
        new Request { Id = 3, Info = new() { Number = "1" } },
        new Request { Id = 4, Info = new() { Text = "2" } },
        new Request { Id = 5, Info = new() { Number = "1", Text = "2" } });

    await context.SaveChangesAsync();
}

using (var context = new SomeDbContext())
{
    var all = context.Set<Request>().ToList();

    foreach (var request in all)
    {
        Console.WriteLine($"{request.Id}: {(request.Info == null ? "no" : "yes")}");
    }
}

using (var context = new SomeDbContext())
{
    var all = context
        .Set<Request>().Select(
            e => new RequestDto
            {
                Id = e.Id,
                Info = e.Info == null
                    ? null
                    : new RequestInfoDto { Number = e.Info.Number, Text = e.Info.Text }
            })
        .ToList();

    foreach (var request in all)
    {
        Console.WriteLine($"{request.Id}: {(request.Info == null ? "no" : "yes")}");
    }
}

public class SomeDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

     protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Request>();//.Navigation(e => e.Info).IsRequired();
    }
}

public class Request
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }
    public RequestInfo Info { get; set; }
}

[Owned]
public class RequestInfo
{
    public string Text { get; set; }
    public string Number { get; set; }
}

public class RequestDto
{
    public int Id { get; set; }
    public RequestInfoDto Info { get; set; }
}

public class RequestInfoDto
{
    public string Text { get; set; }
    public string Number { get; set; }
}

Current output:

warn: 7/19/2023 17:19:12.881 CoreEventId.SensitiveDataLoggingEnabledWarning[10400] (Microsoft.EntityFrameworkCore.Infrastructure)
      Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development.
warn: 7/19/2023 17:19:12.938 RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning[20606] (Microsoft.EntityFrameworkCore.Model.Validation)
      The entity type 'RequestInfo' is an optional dependent using table sharing without any required non shared property that could be used to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query. Add a required pr
operty to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.
info: 7/19/2023 17:19:13.342 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (31ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1
info: 7/19/2023 17:19:13.382 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      IF SERVERPROPERTY('EngineEdition') <> 5
      BEGIN
          ALTER DATABASE [AllTogetherNow] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
      END;
info: 7/19/2023 17:19:13.393 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (11ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      DROP DATABASE [AllTogetherNow];
warn: 7/19/2023 17:19:13.416 RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning[20606] (Microsoft.EntityFrameworkCore.Model.Validation)
      The entity type 'RequestInfo' is an optional dependent using table sharing without any required non shared property that could be used to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query. Add a required pr
operty to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.
info: 7/19/2023 17:19:13.566 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (144ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      CREATE DATABASE [AllTogetherNow];
info: 7/19/2023 17:19:13.599 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (32ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      IF SERVERPROPERTY('EngineEdition') <> 5
      BEGIN
          ALTER DATABASE [AllTogetherNow] SET READ_COMMITTED_SNAPSHOT ON;
      END;
info: 7/19/2023 17:19:13.603 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1
info: 7/19/2023 17:19:13.666 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [Request] (
          [Id] int NOT NULL,
          [Info_Text] nvarchar(max) NULL,
          [Info_Number] nvarchar(max) NULL,
          CONSTRAINT [PK_Request] PRIMARY KEY ([Id])
      );
warn: 7/19/2023 17:19:13.822 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update)
      The entity of type 'RequestInfo' with primary key values {RequestId: 2} is an optional dependent using table sharing. The entity does not have any property with a non-null value to identify whether the entity exists. This means that when it is queried no object instance will be created instead of an insta
nce with all properties set to null values. Any nested dependents will also be lost. Either don't save any instance with only null values or mark the incoming navigation as required in the model.
info: 7/19/2023 17:19:13.874 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (28ms) [Parameters=[@p0='1', @p1='2', @p2=NULL (Size = 4000), @p3=NULL (Size = 4000), @p4='3', @p5='1' (Size = 4000), @p6=NULL (Size = 4000), @p7='4', @p8=NULL (Size = 4000), @p9='2' (Size = 4000), @p10='5', @p11='1' (Size = 4000), @p12='2' (Size = 4000)], CommandType='Text', CommandTim
eout='30']
      SET NOCOUNT ON;
      INSERT INTO [Request] ([Id])
      VALUES (@p0);
      INSERT INTO [Request] ([Id], [Info_Number], [Info_Text])
      VALUES (@p1, @p2, @p3),
      (@p4, @p5, @p6),
      (@p7, @p8, @p9),
      (@p10, @p11, @p12);
info: 7/19/2023 17:19:14.094 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [r].[Id], [r].[Info_Number], [r].[Info_Text]
      FROM [Request] AS [r]
1: no
2: no
3: yes
4: yes
5: yes
info: 7/19/2023 17:19:14.228 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [r].[Id], CASE
          WHEN [r].[Info_Number] IS NULL OR [r].[Info_Text] IS NULL THEN CAST(1 AS bit)
          ELSE CAST(0 AS bit)
      END, [r].[Info_Number], [r].[Info_Text]
      FROM [Request] AS [r]
1: no
2: no
3: no
4: no
5: yes

EF Core 6 output:

warn: 7/19/2023 17:16:25.340 CoreEventId.SensitiveDataLoggingEnabledWarning[10400] (Microsoft.EntityFrameworkCore.Infrastructure)
      Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development.
warn: 7/19/2023 17:16:25.383 RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning[20606] (Microsoft.EntityFrameworkCore.Model.Validation)
      The entity type 'RequestInfo' is an optional dependent using table sharing without any required non shared property that could be used to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query. Add a required pr
operty to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.
info: 7/19/2023 17:16:25.493 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 6.0.20 initialized 'SomeDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.20' with options: SensitiveDataLoggingEnabled
info: 7/19/2023 17:16:25.755 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (29ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1
info: 7/19/2023 17:16:25.793 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      IF SERVERPROPERTY('EngineEdition') <> 5
      BEGIN
          ALTER DATABASE [AllTogetherNow] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
      END;
info: 7/19/2023 17:16:25.807 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (14ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      DROP DATABASE [AllTogetherNow];
warn: 7/19/2023 17:16:25.831 RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning[20606] (Microsoft.EntityFrameworkCore.Model.Validation)
      The entity type 'RequestInfo' is an optional dependent using table sharing without any required non shared property that could be used to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query. Add a required pr
operty to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.
info: 7/19/2023 17:16:25.984 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (140ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      CREATE DATABASE [AllTogetherNow];
info: 7/19/2023 17:16:26.017 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (33ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      IF SERVERPROPERTY('EngineEdition') <> 5
      BEGIN
          ALTER DATABASE [AllTogetherNow] SET READ_COMMITTED_SNAPSHOT ON;
      END;
info: 7/19/2023 17:16:26.021 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT 1
info: 7/19/2023 17:16:26.084 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [Request] (
          [Id] int NOT NULL,
          [Info_Text] nvarchar(max) NULL,
          [Info_Number] nvarchar(max) NULL,
          CONSTRAINT [PK_Request] PRIMARY KEY ([Id])
      );
warn: 7/19/2023 17:16:26.212 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update)
      The entity of type 'RequestInfo' with primary key values {RequestId: 2} is an optional dependent using table sharing. The entity does not have any property with a non-default value to identify whether the entity exists. This means that when it is queried no object instance will be created instead of an in
stance with all properties set to default values. Any nested dependents will also be lost. Either don't save any instance with only default values or mark the incoming navigation as required in the model.
info: 7/19/2023 17:16:26.250 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (20ms) [Parameters=[@p0='1', @p1='2', @p2=NULL (Size = 4000), @p3=NULL (Size = 4000), @p4='3', @p5='1' (Size = 4000), @p6=NULL (Size = 4000), @p7='4', @p8=NULL (Size = 4000), @p9='2' (Size = 4000), @p10='5', @p11='1' (Size = 4000), @p12='2' (Size = 4000)], CommandType='Text', CommandTim
eout='30']
      SET NOCOUNT ON;
      INSERT INTO [Request] ([Id])
      VALUES (@p0);
      INSERT INTO [Request] ([Id], [Info_Number], [Info_Text])
      VALUES (@p1, @p2, @p3),
      (@p4, @p5, @p6),
      (@p7, @p8, @p9),
      (@p10, @p11, @p12);
info: 7/19/2023 17:16:26.288 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 6.0.20 initialized 'SomeDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.20' with options: SensitiveDataLoggingEnabled
info: 7/19/2023 17:16:26.487 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [r].[Id], [r].[Info_Number], [r].[Info_Text]
      FROM [Request] AS [r]
1: no
2: no
3: yes
4: yes
5: yes
info: 7/19/2023 17:16:26.533 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 6.0.20 initialized 'SomeDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.20' with options: SensitiveDataLoggingEnabled
info: 7/19/2023 17:16:26.605 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [r].[Id], CASE
          WHEN ([r].[Info_Number] IS NULL) AND ([r].[Info_Text] IS NULL) THEN CAST(1 AS bit)
          ELSE CAST(0 AS bit)
      END, [r].[Info_Number], [r].[Info_Text]
      FROM [Request] AS [r]
1: no
2: no
3: yes
4: yes
5: yes

ajcvickers avatar Jul 19 '23 16:07 ajcvickers

reopening for patch

maumar avatar Sep 14 '23 21:09 maumar