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

Optional dependent null #24856

Merged
merged 29 commits into from
May 31, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fdb07fc
Perf: unset EnableContentResponseOnWrite #22999
umitkavala Nov 13, 2020
a772ec2
Revert "Perf: unset EnableContentResponseOnWrite #22999"
umitkavala Nov 13, 2020
18b340b
Merge remote-tracking branch 'upstream/main' into main
umitkavala Nov 16, 2020
cce3889
Merge remote-tracking branch 'upstream/main' into main
umitkavala Dec 1, 2020
b8ea6fd
Merge remote-tracking branch 'upstream/main' into main
umitkavala Dec 2, 2020
bd2f03d
Merge remote-tracking branch 'upstream/main' into main
umitkavala Apr 3, 2021
ffdfd9d
Merge remote-tracking branch 'upstream/main' into main
kavalau Apr 9, 2021
0e13a04
Merge remote-tracking branch 'upstream/main' into optional-dependent-…
umitkavala May 6, 2021
495e910
Added test to reproduce issue.
umitkavala May 8, 2021
6e613f9
Log optional dependent
umitkavala May 11, 2021
8dfc5e8
Complete method & Add new test
umitkavala May 15, 2021
7ae83fe
refactor to fix existing test case fails
umitkavala May 16, 2021
0248223
Update SharedTableEntryMap.cs
umitkavala May 16, 2021
0156282
Update SharedTableEntryMap.cs
umitkavala May 16, 2021
de2ce07
Merge remote-tracking branch 'upstream/main' into optional-dependent-…
umitkavala May 24, 2021
d4c962f
Move check to ModificationCommand.GenerateColumnModifications
umitkavala May 24, 2021
b90d566
Fix failed test cases.
umitkavala May 24, 2021
cb29589
Update ModificationCommand.cs
umitkavala May 25, 2021
f46926f
log warning instead throw exception
umitkavala May 25, 2021
ca9ff1f
Update ModificationCommand.cs
umitkavala May 25, 2021
b882981
Revert "Update ModificationCommand.cs"
umitkavala May 25, 2021
978befd
Revert "log warning instead throw exception"
umitkavala May 25, 2021
fdda7a7
pass logger to ModificationCommand constructor
umitkavala May 25, 2021
3ad496a
pass new parameter in tests
umitkavala May 25, 2021
de6618e
add sensitive data to event data. typo on warning message
umitkavala May 26, 2021
82046e5
create new eventId & move models to under base test classs
kavalau May 26, 2021
f2579fe
update comments
umitkavala May 27, 2021
c3b64f7
Update RelationalEventId.cs
umitkavala May 28, 2021
5728d14
test fix
umitkavala May 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2964,5 +2964,32 @@ public static void BatchExecutorFailedToReleaseSavepoint(
diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
}
}

/// <summary>
/// Logs the <see cref="RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning" /> event.
umitkavala marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <param name="diagnostics"> The diagnostics logger to use. </param>
/// <param name="entityType"> The entity type. </param>
public static void OptionalDependentWithAllNullPropertiesWarning(
this IDiagnosticsLogger<DbLoggerCategory.Update> diagnostics,
IEntityType entityType)
{
var definition = RelationalResources.LogOptionalDependentWithoutIdentifyingProperty(diagnostics);

if (diagnostics.ShouldLog(definition))
{
definition.Log(diagnostics, entityType.DisplayName());
}

if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
{
var eventData = new EntityTypeEventData(
definition,
OptionalDependentWithoutIdentifyingPropertyWarning,
entityType);

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ protected virtual IEnumerable<ModificationCommand> CreateModificationCommands(
entry,
(n, s, c) => new ModificationCommand(n, s, generateParameterName, _sensitiveLoggingEnabled, c));
isMainEntry = sharedCommandsMap.IsMainEntry(entry);

if (!isMainEntry)
{
if(sharedCommandsMap.IsOptionalWithNull(entry))
{
Dependencies.UpdateLogger.OptionalDependentWithAllNullPropertiesWarning(entry.EntityType);
}
}
}
else
{
Expand Down
51 changes: 51 additions & 0 deletions src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,57 @@ public virtual TValue GetOrAddValue(IUpdateEntry entry, SharedTableEntryValueFac
public virtual bool IsMainEntry(IUpdateEntry entry)
=> !_table.GetRowInternalForeignKeys(entry.EntityType).Any();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool IsOptionalWithNull(IUpdateEntry entry)
{
var principalEntityTypesMap = new Dictionary<IEntityType, bool>();

var optional = GetPrincipalEntityTypes(entry.EntityType);
umitkavala marked this conversation as resolved.
Show resolved Hide resolved

var nullableWithNull = true;

if (!optional)
{
return false;
}

foreach (var property in entry.EntityType.GetProperties())
{
if (property.IsPrimaryKey())
umitkavala marked this conversation as resolved.
Show resolved Hide resolved
{
continue;
}

if(entry.GetCurrentValue(property) is not null)
{
nullableWithNull = false;
}
}

bool GetPrincipalEntityTypes(IEntityType entityType)
{
if (!principalEntityTypesMap.TryGetValue(entityType, out var optional))
{
foreach (var foreignKey in entityType.FindForeignKeys(entityType.FindPrimaryKey()!.Properties))
{
var principalEntityType = foreignKey.PrincipalEntityType;
var innerOptional = GetPrincipalEntityTypes(principalEntityType.GetRootType());

optional |= !foreignKey.IsRequiredDependent | innerOptional;
}
}

return optional;
}

return nullableWithNull;
}

private IUpdateEntry GetMainEntry(IUpdateEntry entry)
{
var entityType = entry.EntityType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.TestModels.QueryModel;
using Microsoft.EntityFrameworkCore.TestModels.TransportationModel;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Xunit;
Expand Down Expand Up @@ -548,10 +551,92 @@ public virtual async Task Optional_dependent_materialized_when_no_properties()
}
}

[ConditionalFact]
public virtual async Task Warn_when_save_optional_dependent_with_null_values()
{
await InitializeSharedAsync(
modelBuilder =>
{
modelBuilder.Entity<MeterReadingDetail>(
dob =>
{
dob.ToTable("MeterReadings");
dob.Property(o => o.ReadingStatus).HasColumnName("ReadingStatus");
});

modelBuilder.Entity<MeterReading>(
ob =>
{
ob.ToTable("MeterReadings");
ob.Property(o => o.ReadingStatus).HasColumnName("ReadingStatus");
ob.HasOne(o => o.MeterReadingDetails).WithOne()
.HasForeignKey<MeterReadingDetail>(o => o.Id);
});
}, seed: false
);

umitkavala marked this conversation as resolved.
Show resolved Hide resolved
using var context = CreateSharedContext();
var scooterEntry = context.Add(
new MeterReading
{
MeterReadingDetails = new MeterReadingDetail()
});

var expected = RelationalResources.LogOptionalDependentWithoutIdentifyingProperty(new TestLogger<TestRelationalLoggingDefinitions>()).GenerateMessage(nameof(MeterReadingDetail));

context.SaveChanges();

var log = TestSqlLoggerFactory.Log.Single(l => l.Level == Extensions.Logging.LogLevel.Warning);

Assert.Equal(expected, log.Message);
}

[ConditionalFact]
public virtual async Task No_warn_when_save_optional_dependent_at_least_one_non_null()
{
await InitializeSharedAsync(
modelBuilder =>
{
modelBuilder.Entity<MeterReadingDetail>(
dob =>
{
dob.ToTable("MeterReadings");
dob.Property(o => o.ReadingStatus).HasColumnName("ReadingStatus");
});

modelBuilder.Entity<MeterReading>(
ob =>
{
ob.ToTable("MeterReadings");
ob.Property(o => o.ReadingStatus).HasColumnName("ReadingStatus");
ob.HasOne(o => o.MeterReadingDetails).WithOne()
.HasForeignKey<MeterReadingDetail>(o => o.Id);
});
}, seed: false
);

using var context = CreateSharedContext();
var scooterEntry = context.Add(
new MeterReading
{
MeterReadingDetails = new MeterReadingDetail()
{
CurrentRead = "123"
}
});

context.SaveChanges();

var log = TestSqlLoggerFactory.Log.SingleOrDefault(l => l.Level == Extensions.Logging.LogLevel.Warning);

Assert.Null(log.Message);
}

protected override string StoreName { get; } = "TableSplittingTest";
protected TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
protected ContextFactory<TransportationContext> ContextFactory { get; private set; }
protected ContextFactory<SharedTableContext> SharedContextFactory { get; private set; }

protected void AssertSql(params string[] expected)
=> TestSqlLoggerFactory.AssertBaseline(expected);
Expand Down Expand Up @@ -580,14 +665,27 @@ protected async Task InitializeAsync(Action<ModelBuilder> onModelCreating, bool
onModelCreating, shouldLogCategory: _ => true, seed: seed ? c => c.Seed() : null);
}

protected async Task InitializeSharedAsync(Action<ModelBuilder> onModelCreating, bool seed = true)
{
SharedContextFactory = await InitializeAsync<SharedTableContext>(
onModelCreating,
shouldLogCategory: _ => true,
onConfiguring: options => options.ConfigureWarnings(w => w.Log(RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning)
));
}

protected virtual TransportationContext CreateContext()
=> ContextFactory.CreateContext();

protected virtual SharedTableContext CreateSharedContext()
=> SharedContextFactory.CreateContext();

public override void Dispose()
{
base.Dispose();

ContextFactory = null;
SharedContextFactory = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.EntityFrameworkCore.TestModels.QueryModel
{
public class MeterReading
{
public int Id { get; set; }
public MeterReadingStatus? ReadingStatus { get; set; }
public MeterReadingDetail MeterReadingDetails { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.EntityFrameworkCore.TestModels.QueryModel
{
public class MeterReadingDetail
{
public int Id { get; set; }
public MeterReadingStatus? ReadingStatus { get; set; }
public string CurrentRead { get; set; }
public string PreviousRead { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.EntityFrameworkCore.TestModels.QueryModel
{
public enum MeterReadingStatus
{
Running=0,
NotAccesible=2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.EntityFrameworkCore.TestUtilities;


namespace Microsoft.EntityFrameworkCore.TestModels.QueryModel
{
public class SharedTableContext : PoolableDbContext
umitkavala marked this conversation as resolved.
Show resolved Hide resolved
{
public SharedTableContext(DbContextOptions options)
: base(options)
{
}

public DbSet<MeterReading> MeterReadings { get; set; }
public DbSet<MeterReadingDetail> MeterReadingDetails { get; set; }
}
}