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

Looking for suggestions for marking entities with a property using custom serialization logic as dirty. #23789

Closed
atrauzzi opened this issue Jan 1, 2021 · 9 comments
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@atrauzzi
Copy link

atrauzzi commented Jan 1, 2021

Cross posting my StackOverflow question here, but I feel like all the experts who will be able to answer my question live 'round these parts. 😉


I have the following:

public interface Discriminable
{
}

public interface Configurable
{
    public Discriminable Configuration { get; set; }
}

public class MyData : Configurable
{
    public Guid Id { get; set; }

    [Column(TypeName="jsonb")]
    public Discriminable Configuration { get; set; }

    public string Name { get; set; }
}

public class OtherData
{
    public Guid Id { get; set; }
    public MyData MyData { get; set; }
}

//
// Inside of my DbContext
//

modelBuilder
    .Entity<Configurable>()
    .Property((configurable) => configurable.Configuration)
    .HasConversion(
        (configuration) => MySerializationLogic(configuration),
        (json) => MyDeserializationLogic(json)
    );

//

I'm running into an issue right now where I can save and load data fine, so long as I manually force the entity to be marked as dirty. Either by simply scanning for all entities of type Configurable and then manually marking them as dirty, or by telling the change tracker in my procedural code that they're dirty - somewhat painstakingly.

This is necessary because the values inside instances of Discriminable aren't noticed by the change tracker and as such, won't queue the containing Configurable instances for updates by marking them as dirty.

Sadly, my current approach results in a problem in some scenarios, like when I add MyData to OtherData and then try to save it. Because the MyData instance is already in the change tracker, EF attempts to update twice and I end up getting this exception:

Database operation expected to affect 1 row(s) but actually affected 0 row(s).

My question at this point: Is there any way for me to lean on the change tracker to notice when values inside of my Discriminable instances have changed? Are there potentially any other techniques that I can use to avoid having to add manual state tracking throughout my procedural code?

@anranruye
Copy link

anranruye commented Jan 1, 2021

@atrauzzi

You must implement System.ComponentModel.INotifyPropertyChanged interface for your Discriminable entity class so that you can get notifications when values inside instances of Discriminable are changed. Then you can handle these changes.

Here is an example:

//Entities:
...
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
...
    public class BindableBase : INotifyPropertyChanged
    {
        protected virtual bool SetProperty<T>(ref T item, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(item, value)) return false;
            item = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class JsonObject : BindableBase, INotifyPropertyChanged
    {
        private string stringJsonProperty;
        private int intJsonProperty;

        public int IntJsonProperty { get => intJsonProperty; set => SetProperty(ref intJsonProperty, value); }

        public string StringJsonProperty { get => stringJsonProperty; set => SetProperty(ref stringJsonProperty, value); }
    }

    public class MyEntity : BindableBase, INotifyPropertyChanged
    {
        private JsonObject json;
        private int id;

        public int Id { get => id; set => SetProperty(ref id, value); }

        public JsonObject Json 
        { 
            get => json; 
            set
            {
                if (json != null)
                {
                    json.PropertyChanged -= JsonChanged;
                }
                SetProperty(ref json, value);
                if (json != null)
                {
                    json.PropertyChanged += JsonChanged;
                }
            } 
        }

        private void JsonChanged(object sender, PropertyChangedEventArgs e)
        {
            OnPropertyChanged(nameof(Json));
        }
    }
...
    
//DbContext:
...
    using Newtonsoft.Json;
...
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<MyEntity>()
                .HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangedNotifications)
                .Property(x => x.Json)
                .UsePropertyAccessMode(PropertyAccessMode.Property)
                .HasConversion(
                    obj => JsonConvert.SerializeObject(obj),
                    json => JsonConvert.DeserializeObject<JsonEntity>(json)
                );
        }
    }
...

We can use the following code to change values inside the JsonObject instance:

            var e = db.MyEntities.FirstOrDefault();
            e.Json.StringJsonProperty = "I like ef core!" ;
            db.SaveChanges();

@atrauzzi
Copy link
Author

atrauzzi commented Jan 1, 2021

Is there any way of doing this without having the component model namespace bleed into my domain and having to write all that extra code in my models themselves?

Or I guess alternatively, leverage something similar to the change tracker? I don't have to implement any interfaces on my regular domain objects for EF to notice changes to their regular properties...

@ajcvickers
Copy link
Contributor

@atrauzzi You need to implement a value comparer, as described here: https://docs.microsoft.com/en-us/ef/core/modeling/value-comparers

For example:

        modelBuilder
            .Entity<MyData>()
            .Property(configurable => configurable.Configuration)
            .HasConversion(
                configuration => JsonSerializer.Serialize(configuration, null),
                json => JsonSerializer.Deserialize<Discriminable>(json, null),
                new ValueComparer<IDiscriminable>(
                    (l, r) => l.Equals(r),
                    v => v.GetHashCode(),
                    v => (IDiscriminable)new Discriminable()));

However, your code is also trying to map the (unusually named) interface Configurable as an entity type, which won't work. Entity types must be concrete instances.

@atrauzzi
Copy link
Author

atrauzzi commented Jan 1, 2021

Ah yes, my apologies, that should have been the concrete entity type.

@atrauzzi
Copy link
Author

atrauzzi commented Jan 1, 2021

On a separate note @ajcvickers -- Is there any way that EF could make its change tracking mechanism available for people to tap into instead of requiring them to write what will ultimately be a shabby duplication of EF's own change tracking logic?

My assumption is that somewhere in the codebase of EF, there's something that's really good at just keeping a list of objects to watch and noticing when any of their properties change. I'd love to have access to that functionality, which was largely my motivation for opening #23790, separate to this.

Taking #23790 a little further, the overall idea was, to get the same change tracking semantics for my sub-object as what EF already does in a handy way.

I just want it to trigger a dirty state on the containing entity, not the tracked non-entity object itself...

@ajcvickers
Copy link
Contributor

@atrauzzi Value converters and comparers are the way to hook into this mechanism. That being said, you could replace the IChangeDetector, which is the internal service that EF Core uses for change detection. I wouldn't recommend it, and you will be using internal code that may be broken in any release, but feel free to play with it.

@atrauzzi
Copy link
Author

atrauzzi commented Jan 1, 2021

Well, I'm not dispiting how to hook that mechamism, let's not focus too hard on that for the rest of this. I understand that it exists and will give me a very convoluted path to what I want. But now I'm trying to make a case for an opportunity EF has here:

I'm not looking to replace the change detector. I just want to use the one that's already there instead of incurring the overhead and maintenance of any change tracker I'd have to write myself (and probably get wrong) on top of EF's battle-tested change tracker.

I don't think the objective of #23790 is outside of EF's scope either considering that many people have and will continue to come to need this functionality. Especially with the rise in popularity of JSON columns.

At the core of all this is a two-part feature request:

  1. To expand the flexibility of the change tracker to simply know about objects that aren't entities in the DbContext.
  2. To maybe include some convenience methods to map the most commonly desired behaviour, which is that if a tracked non-entity object changes, flag a corresponding tracked entity as dirty. (Although with 1., an IChangeDetector might be much more trivial to implement.)

Genuinely, I don't think I'm being biased by my own need when I say: People would probably find this quite useful. 🤞

@ajcvickers
Copy link
Contributor

  1. To expand the flexibility of the change tracker to simply know about objects that aren't entities in the DbContext.

This is what value converters and value comparers do.

To maybe include some convenience methods to map the most commonly desired behaviour, which is that if a tracked non-entity object changes, flag a corresponding tracked entity as dirty. (Although with 1., an IChangeDetector might be much more trivial to implement.)

I really don't think this is necessary, but maybe I don't understand what you mean by a "tracked non-entity object ". To me, this can only mean an object referenced in a property of an entity. Which then means again that value converters and value comparers are the way to do this. If the comparer reports that the non-tracked object has changed, then the entity is marked as modified.

@atrauzzi
Copy link
Author

atrauzzi commented Jan 1, 2021

Let me answer in the other thread...

@atrauzzi atrauzzi closed this as completed Jan 1, 2021
@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label Jan 1, 2021
@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

3 participants