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

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

Closed
d-kistanov-parc opened this issue May 30, 2023 · 4 comments · Fixed by #31714
Closed
Assignees
Labels
area-query closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Milestone

Comments

@d-kistanov-parc
Copy link

d-kistanov-parc commented May 30, 2023

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" ?

@georgyevv
Copy link

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
Copy link

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
                }
        })

@ajcvickers
Copy link
Contributor

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

@AndriySvyryd AndriySvyryd added this to the 8.0.0 milestone Jul 19, 2023
maumar added a commit that referenced this issue Sep 12, 2023
…with owned type default value

In optional dependent table sharing scenario, when comparing the dependent to null we first look for any required properties (at least one of them needs to be null), and if we don't have any required properties we look at optional ones (even though this is somewhat ambiguous).
Error was that we did similar check as with required properties (i.e. at least one of them must be non), but what we should be doing is checking that all of them are null.

Fixes #30996
maumar added a commit that referenced this issue Sep 12, 2023
…with owned type default value

In optional dependent table sharing scenario, when comparing the dependent to null we first look for any required properties (at least one of them needs to be null), and if we don't have any required properties we look at optional ones (even though this is somewhat ambiguous).
Error was that we did similar check as with required properties (i.e. at least one of them must be null), but what we should be doing is checking that all of them are null.

Fixes #30996
@maumar maumar added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Sep 12, 2023
maumar added a commit that referenced this issue Sep 13, 2023
…with owned type default value

In optional dependent table sharing scenario, when comparing the dependent to null we first look for any required properties (at least one of them needs to be null), and if we don't have any required properties we look at optional ones (even though this is somewhat ambiguous).
Error was that we did similar check as with required properties (i.e. at least one of them must be null), but what we should be doing is checking that all of them are null.

Fixes #30996
maumar added a commit that referenced this issue Sep 14, 2023
…with owned type default value (#31714)

In optional dependent table sharing scenario, when comparing the dependent to null we first look for any required properties (at least one of them needs to be null), and if we don't have any required properties we look at optional ones (even though this is somewhat ambiguous).
Error was that we did similar check as with required properties (i.e. at least one of them must be null), but what we should be doing is checking that all of them are null.

Fixes #30996
maumar added a commit that referenced this issue Sep 14, 2023
…with owned type default value

In optional dependent table sharing scenario, when comparing the dependent to null we first look for any required properties (at least one of them needs to be null), and if we don't have any required properties we look at optional ones (even though this is somewhat ambiguous). Error was that we did similar check as with required properties (i.e. at least one of them must be null, but what we should be doing is checking that all of them are null.

Fixes #30996
@maumar
Copy link
Contributor

maumar commented Sep 14, 2023

reopening for patch

@maumar maumar reopened this Sep 14, 2023
@maumar maumar modified the milestones: 8.0.0, 7.0.12 Sep 14, 2023
@ajcvickers ajcvickers modified the milestones: 7.0.12, 7.0.13 Oct 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-query closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Projects
None yet
5 participants