DateTime? and IsChanged property with null values

Posts   
 
    
usschad
User
Posts: 74
Joined: 11-Sep-2008
# Posted on: 15-Nov-2024 19:19:18   

My Environment: LLBLGen: 5.11.1 - 19-Dec-2023 (I checked the log and there didn't appear to be a fix to the issue I 'think' I see) Runtime: 5.11.0.0 No exception, just unexpected behavior Adapter Template with .NET 8 (also appears in 6) Azure SQL DB (though this concern is in code, after Entity Initialization and setting fields in memory)

The story: I'm looking to evaluate the IsChanged property after assigning some values from a dto. I have a new entity that I initialize. I then take a dto and copy the value from its DateTime? field to the entity's DateTime? field. In the case where the dto field is null, I would expect that when the code sets the entity field to null, that IsChanged would remain false. However, I am getting IsChanged == true.

I wrote a unit test to show that this behavior works as I am expecting with a string field, but not with a DateTime? field.

        [Fact]
        public void NullableDateTimeIsChangedWorks()
        {
            var facilityConfig = new FacilityConfigEntity {};

            var fieldIndexToUpdate = (int)FacilityConfigFieldIndex.UpdateBy; // USING A STRING FIELD
            Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].DbValue);
            Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].CurrentValue);
            Assert.False(facilityConfig.Fields[fieldIndexToUpdate].IsChanged);

            facilityConfig.UpdateBy = null; // SET STRING TO NULL

            Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].DbValue);
            Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].CurrentValue);
            Assert.False(facilityConfig.Fields[fieldIndexToUpdate].IsChanged); // PASSES!

            fieldIndexToUpdate = (int)FacilityConfigFieldIndex.EndDate; // USING NULLABLE DATETIME FIELD
            Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].DbValue);
            Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].CurrentValue);
            Assert.False(facilityConfig.Fields[fieldIndexToUpdate].IsChanged); 

            facilityConfig.EndDate = null; // SET DATETIME? TO NULL

            Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].DbValue);
            Assert.Null(facilityConfig.Fields[fieldIndexToUpdate].CurrentValue);
            Assert.False(facilityConfig.Fields[fieldIndexToUpdate].IsChanged); // FAILS!
        }

In the code above, the IsChanged is still false after setting the value to null (as expected). However, for the DateTime? it fails. I would expect the same behavior as the string.

In reality, we are trying to update entities that are fetched from the database, but I thought this simplified the reproduction of the problem just using a new entity. Of course, the same problem occurs when we do that.

Looking at this thread: https://www.llblgen.com/tinyforum/Thread/16049/1 I think my expectations are right that when setting the DateTime? field to a null value, that IsChanged should remain false just like the string field does.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 16-Nov-2024 08:41:46   

We'll look into it!

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 18-Nov-2024 09:41:12   

I can't reproduce the difference between string and datetime: both change

This is on Northwind, Customer.City is a nullable string field. Order.OrderDate is a nullable DateTime. I think I've rebuilt your test correctly? If not, please correct me. I've also added checking for IsNull which are false, because they check on the DB Value and on a new entity there are no db values so they report false.

var customer = new CustomerEntity { };

Assert.AreEqual(string.Empty, customer.City);
Assert.IsNull(customer.Fields[(int)CustomerFieldIndex.City].DbValue);
Assert.IsNull(customer.Fields[(int)CustomerFieldIndex.City].CurrentValue);
Assert.IsNull(customer.Fields.GetDbValue((int)CustomerFieldIndex.City));
Assert.IsNull(customer.Fields.GetCurrentValue((int)CustomerFieldIndex.City));
Assert.IsFalse(customer.Fields[(int)CustomerFieldIndex.City].IsChanged);
Assert.IsFalse(customer.Fields.GetIsNull((int)CustomerFieldIndex.City));
Assert.IsFalse(customer.Fields[(int)CustomerFieldIndex.City].IsNull);

customer.City = null;
Assert.AreEqual(string.Empty, customer.City);
Assert.IsNull(customer.Fields[(int)CustomerFieldIndex.City].DbValue);
Assert.IsNull(customer.Fields[(int)CustomerFieldIndex.City].CurrentValue);
Assert.IsNull(customer.Fields.GetDbValue((int)CustomerFieldIndex.City));
Assert.IsNull(customer.Fields.GetCurrentValue((int)CustomerFieldIndex.City));
Assert.IsTrue(customer.Fields[(int)CustomerFieldIndex.City].IsChanged);         // SUCCEEDS
Assert.IsFalse(customer.Fields.GetIsNull((int)CustomerFieldIndex.City));        // because no db data is present
Assert.IsFalse(customer.Fields[(int)CustomerFieldIndex.City].IsNull);           // because no db data is present

var order = new OrderEntity { };

Assert.IsNull(order.OrderDate);
Assert.IsNull(order.Fields[(int)OrderFieldIndex.OrderDate].DbValue);
Assert.IsNull(order.Fields[(int)OrderFieldIndex.OrderDate].CurrentValue);
Assert.IsNull(order.Fields.GetDbValue((int)OrderFieldIndex.OrderDate));
Assert.IsNull(order.Fields.GetCurrentValue((int)OrderFieldIndex.OrderDate));
Assert.IsFalse(order.Fields[(int)OrderFieldIndex.OrderDate].IsChanged);
Assert.IsFalse(order.Fields.GetIsNull((int)OrderFieldIndex.OrderDate));
Assert.IsFalse(order.Fields[(int)OrderFieldIndex.OrderDate].IsNull);

order.OrderDate = null;
Assert.IsNull(order.OrderDate);
Assert.IsNull(order.Fields[(int)OrderFieldIndex.OrderDate].DbValue);
Assert.IsNull(order.Fields[(int)OrderFieldIndex.OrderDate].CurrentValue);
Assert.IsNull(order.Fields.GetDbValue((int)OrderFieldIndex.OrderDate));
Assert.IsNull(order.Fields.GetCurrentValue((int)OrderFieldIndex.OrderDate));
Assert.IsTrue(order.Fields[(int)OrderFieldIndex.OrderDate].IsChanged);      // SUCCEEDS
Assert.IsFalse(order.Fields.GetIsNull((int)OrderFieldIndex.OrderDate));     // because no db data is present
Assert.IsFalse(order.Fields[(int)OrderFieldIndex.OrderDate].IsNull);        // because no db data is present

The reason this enforces the field to be marked changed is that null is a valid value for the fields, as they're nullable. So we do mark them as changed. This is done to make sure they're updated in save (as you might want to update an entity which currently holds a value for the field and now receives 'NULL' for it so a default constraint might run). As the initial state of the field is null not changed, it won't be persisted in an update so we mark it as changed. The practical value the field has (null) didn't effectively change, but the initial state of the field is really 'undefined', not 'null': if you see the initial entity state as undefined, it might be more clear why setting a nullable field to null in that case marks the field as changed.

Why your string field isn't marked as changed however is weird. I can't reproduce that.

Frans Bouma | Lead developer LLBLGen Pro
thargenediad avatar
Posts: 1
Joined: 19-Nov-2024
# Posted on: 19-Nov-2024 23:58:05   
Assert.IsTrue(order.Fields[(int)OrderFieldIndex.OrderDate].IsChanged);      // SUCCEEDS       <-- this seems like a bug
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 20-Nov-2024 08:44:49   

thargenediad wrote:

Assert.IsTrue(order.Fields[(int)OrderFieldIndex.OrderDate].IsChanged);      // SUCCEEDS       <-- this seems like a bug

No, as I explained above: the field has changed from undefined to null, as null is a valid value and by having it marked as changed it's included in an update statement. The field isn't seen as having a null value in a new entity, mind you.

Also, the string field in your test behaves differently than in mine, so something's not the same in my test wrt that field compared to yours.

Frans Bouma | Lead developer LLBLGen Pro
usschad
User
Posts: 74
Joined: 11-Sep-2008
# Posted on: 20-Nov-2024 18:31:10   

Hi Frans, I see what you mean. Yes, I think you reproduced my test correctly; I think the problem is that I thought the new (unfetched) entity had the same behavior as a fetched entity. If so, sorry about that confusion. I was hoping to simplify my test without having to save because my test entity had a lot of dependencies.

I wrote a new test with a different entity, but same concept, with an entity that didn't have as many dependencies (I excluded the string field because I think I have a separate issue going on there since we're using a string type converter; I can either create a separate post for that, or we can move onto it once I determine what to expect with my DateTime? field):

The story here is that I am initializing an entity, immediately saving to the database (with a refetch).
The DueDate field is nullable and after refetch is still null. I set the DueDate to null, and IsChanged becomes true. Is that still expected here?

EDIT Added a comment in the last line of code below. EDIT

        [Fact]
        public void NullableDateTimeIsChangedWorks()
        {
            var instrument = new IngestInstrumentEntity { };

            using DataAccessAdapter adapter = new DataAccessAdapter();
            adapter.SaveEntity(instrument, true);

            var dueDateField = instrument.Fields[(int)IngestInstrumentFieldIndex.DueDate];
            
            Assert.Null(dueDateField.DbValue);
            Assert.Null(dueDateField.CurrentValue);
            Assert.False(dueDateField.IsChanged);

            instrument.DueDate = null;

            Assert.Null(dueDateField.DbValue);
            Assert.Null(dueDateField.CurrentValue);
            Assert.False(dueDateField.IsChanged);  // THIS FAILS.  Expecting IsChanged == false since the fetched value was already null.
        }

Note: I'm not sure if it makes a difference, but this field is a DateTime2(3) datatype in the database.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 21-Nov-2024 10:16:56   

I can't reproduce it. (with 5.11.4. You could pull the package from nuget and see if it makes a difference, but as you said earlier, there haven't been fixes in this area)

[Test]
public void NullCheckOnRefetchedNullableField()
{
    using(var adapter = new DataAccessAdapter())
    {
        var newCustomer = EntityCreator.CreateNewCustomer(1);
        newCustomer.TestRunId = _testRunID;
        AddressEntity newAddress = EntityCreator.CreateNewAddress(1);
        newAddress.TestRunId = _testRunID;
        newCustomer.VisitingAddress = newAddress;
        newCustomer.BillingAddress = newAddress;
        var newOrder = new OrderEntity();
        newOrder.OrderDate = DateTime.Now;
        newOrder.Customer = newCustomer;
        newOrder.TestRunId = _testRunID;
        bool result = adapter.SaveEntity(newCustomer, true);
        Assert.AreEqual(true, result);
        Assert.AreEqual(EntityState.Fetched, newOrder.Fields.State);

        var shippedDateField = newOrder.Fields[(int)OrderFieldIndex.ShippedDate];
        Assert.Null(shippedDateField.DbValue);
        Assert.Null(shippedDateField.CurrentValue);
        Assert.False(shippedDateField.IsChanged);

        newOrder.ShippedDate = null;
        
        Assert.Null(shippedDateField.DbValue);
        Assert.Null(shippedDateField.CurrentValue);
        Assert.False(shippedDateField.IsChanged);
    }
}

Works, so it doesn't fail on the last line. ShippedDate is a DateTime2 field here too, but that doesn't really matter, changing the fields doesn't interact with the mapping data.

Would it be possible to debug the call path of setting the field's value in your case? The interesting code is in EntityCore<T>.SetValue, around line 3105:

if(FieldUtilities.DetermineIfFieldShouldBeSet(_fields.GetIsChanged(fieldIndex), originalValue, _fields.GetDbValue(fieldIndex), _isNew, valueToSet))

if this succeeds, it'll set the field and mark it as dirty. If not, it'll skip it. In my test, FieldUtilities.DetermineIfFieldShouldBeSet returns false.

Regarding the type converter problem with your string field, let's first get this sorted, it might be the same issue...

Frans Bouma | Lead developer LLBLGen Pro