joelweiss/ChangeTracking

Trying to create a generic method to get a Dictionary of "UpdatedPropertyNames as Keys" and "UpdatedPropertyValue as Value".

Closed this issue · 6 comments

I am trying to create a generic method PublishChangeTrackerMessage which can take 2 objects of similar type and find differences between those 2 objects.
Here is the class that I created. Any class that extends BaseModel and implements IModel could be passed to the method PublishChangeTrackerMessage.

I am able to create the dictionary of PropertyNames and the NewValue for simple classes. If my Class has complex Property i.e. in the below example Models1 class has ChildModels then want to create the dictionary of 'ChildModels' => [new list of all addedItems, ChangedItems, DeletedItems].

When I call CastToIChangeTrackableCollection I am getting Unable to cast object of type 'System.Collections.Generic.List1[System.Object]' to type 'ChangeTracking.IChangeTrackableCollection1[System.Object]'.

Due to Generic types and type conversions I am getting this Casting error. Is there a solution to create the changes elements Dictionary in a different way?

`using ChangeTracking;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
namespace ChangeTracker
{
public abstract class BaseModel : IModel
{
public virtual int Id { get; set; }

    public virtual DateTime UpdatedDateTime { get; set; }

    public void Update(BaseDbModel updatedModel)
    {
        this.Id = updatedModel.Id;
        this.UpdatedDateTime = DateTime.UtcNow;
    }
}
public interface IModel<T1>
{
    public void Update(T1 updatedModel);
}

public class Model1:BaseModel, IModel<Model1>
{
    public virtual string Name { get; set; }
    public virtual int[] intArray { get; set; }
    public virtual IList<ChildModel> childModels { get; set; }
    public void Update(Model1 updatedModel)
    {
        this.Name = updatedModel.Name;
        this.intArray = updatedModel.intArray;
        childModels.Add(new ChildModel() { Id = 4, ChildName = "Added" });
    }
}
public class ChildModel : BaseModel, IModel<Model1>
{
    public virtual string ChildName { get; set; }
}
public class ChangeTrackService<T> where T:IModel<T>
{
    // Output of this method I am expecting Dictionary of "UpdatedPropertyNames as Keys"
    // and "UpdatedPropertyValue as Value".
    // IF the Updated Property is Collection of BaseModel Type then the dictionary Value should have a list of  AddedItems, DeletedItems & ChangeItems
    public Dictionary<string, object> PublishChangeTrackerMessage(T currentModel, T updatedModel)
    {
        var trackable = currentModel.AsTrackable();
        trackable.Update(updatedModel);
        var changedProperties = trackable.CastToIChangeTrackable().ChangedProperties;
        Dictionary<string, object> changes = new Dictionary<string, object>();
        foreach (var changedProperty in changedProperties)
        {
            // if Change property is not a collection
            if (!TrackableObjectCollection(trackable, changedProperty))
            {
                changes.Add(changedProperty, GetPropValue(trackable, changedProperty));
            }
            else  // If its a Trackable collection  
            {
                var changedCollection = GetPropCollectionValue(trackable, changedProperty);
                List<object> collectionChanges = new List<object>();

                // ISSUE: Unable to cast object of type 'System.Collections.Generic.List`1[System.Object]' to type 'ChangeTracking.IChangeTrackableCollection`1[System.Object]'.
                // Due to Generic Type T I have to use GetValue which return object and need to do TypeCasting
                var trackableCollection = changedCollection.CastToIChangeTrackableCollection();
                IEnumerable<object> addedItems = trackableCollection.AddedItems;
                IEnumerable<object> deletedItems = trackableCollection.DeletedItems;
                IEnumerable<object> changedItems = trackableCollection.ChangedItems;
                foreach (var addedItem in addedItems)
                {
                   //Do something
                }
                changes.Add(changedProperty, collectionChanges);
            }
            return changes;
        }
    }
    public object GetPropValue(object src, string propName)
    {
        return src.GetType().GetProperty(propName).GetValue(src, null);
    }
    public IList<object> GetPropCollectionValue(object src, string propName)
    {
        return ((IList<object>)src.GetType().GetProperty(propName).GetValue(src, null)).Cast<object>().ToList();
    }
    public bool TrackableObjectCollection(object src, string propName)
    {
        var propertyType = src.GetType().GetProperty(propName).PropertyType;
        if (propertyType.IsAssignableFrom(typeof(string)))
        {
            return false;
        }
        if (propertyType.GetInterfaces().Any(t => t.IsAssignableFrom(typeof(IEnumerable<>).GetGenericTypeDefinition())))
        {
            // Check if the arugment is object BaseModel Type or not.
            return propertyType.GetGenericArguments().Any(at => at.GetInterfaces().Any(ai => ai.GetGenericArguments().Any(t => t.IsAssignableFrom(typeof(BaseModel)))));
        }
        return false;
    }

}

}
`

The code sample you provided does not compile, please fix it so I can understand better what you are trying to achieve.

Thank you for checking!

I created a repo https://github.com/tgattu/ChangeTrackingFeatureRequest

It has both working solution and not working solution. I am trying to come up a generic method that returns the Dictionary of all the changed Properties and its changes values.
I am trying to get failingChangeTracker method work for any classes that are implements IModel.

is this what you are looking for?

...
else  // If its a Trackable collection  
{
	List<object> collectionChanges = new List<object>();

	object changedValue = trackable.GetType().GetProperty(changedProperty).GetValue(trackable, null);
	Type changeTrackableCollectionType = changedValue.GetType().GetInterface(nameof(IChangeTrackableCollection<object>));
	bool isChangeTrackableCollection = changeTrackableCollectionType != null;
	if (isChangeTrackableCollection)
	{
		IEnumerable<object> addedItems = (IEnumerable<object>)changeTrackableCollectionType.GetProperty(nameof(IChangeTrackableCollection<object>.AddedItems)).GetValue(changedValue);
		IEnumerable<object> deletedItems = (IEnumerable<object>)changeTrackableCollectionType.GetProperty(nameof(IChangeTrackableCollection<object>.DeletedItems)).GetValue(changedValue);
		IEnumerable<object> changedItems = (IEnumerable<object>)changeTrackableCollectionType.GetProperty(nameof(IChangeTrackableCollection<object>.ChangedItems)).GetValue(changedValue);
		foreach (IChangeTrackable addedItem in addedItems)
		{
			Dictionary<string, object> expando = new Dictionary<string, object>();
			expando.Add("ChangeType", "ADD");
			this.CopyFields(addedItem, expando);
			collectionChanges.Add(expando);
		}
		changes.Add(changedProperty, collectionChanges);
	}
}
...

In the above code isChangeTrackableCollection is coming as false. I was expecting that changedValue should be changeTrackableCollectionType i.e. isChangeTrackableCollection should be true.

Model1.childModels collection has changes, I want to read those changes and add to the "changes" dictionary.

I found the solution if I use dynamic and ignore typecasting its working.


// use dynamic type
dynamic changedValue = trackable.GetType().GetProperty(changedProperty).GetValue(trackable, null);
		IEnumerable<object> addedItems = changedValue.AddedItems;
		foreach (IChangeTrackable addedItem in addedItems)
		{
			Dictionary<string, object> expando = new Dictionary<string, object>();
			expando.Add("ChangeType", "ADD");
			this.CopyFields(addedItem, expando);
			collectionChanges.Add(expando);
		}
		changes.Add(changedProperty, collectionChanges);

I am glad you found a solution.