joelweiss/ChangeTracking

Change tracking breaks with string lists

gcottrell13 opened this issue · 3 comments

I ran into a problem with a string list on my model where it would try to track the strings, and it would then die. Is this a known issue, and can you replicate?

I've included a small example that also died in 2.0.13

    public class TestThing
    {
        public virtual IList<string> MyStrings { get; set; }

        public TestThing()
        {
            MyStrings = new List<string>();
        }
    }

// ----------------------------------------------------------------
            var test = new TestThing();
            test.MyStrings.Add("wat");
                

            test = test.AsTrackable();
            test.MyStrings = new List<string>() { "ok", "yea" };

Exception:

|System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.InvalidOperationException: Property Chars is not virtual. Can't track classes with non-virtual properties.
   at ChangeTracking.ChangeTrackingProxyGenerationHook.NonProxyableMemberNotification(Type type, MemberInfo memberInfo)
   at Castle.DynamicProxy.Contributors.MembersCollector.AcceptMethod(MethodInfo method, Boolean onlyVirtuals, IProxyGenerationHook hook)
   at Castle.DynamicProxy.Contributors.WrappedClassMembersCollector.GetMethodToGenerate(MethodInfo method, IProxyGenerationHook hook, Boolean isStandalone)
   at Castle.DynamicProxy.Contributors.MembersCollector.AddMethod(MethodInfo method, IProxyGenerationHook hook, Boolean isStandalone)
   at Castle.DynamicProxy.Contributors.MembersCollector.AddProperty(PropertyInfo property, IProxyGenerationHook hook)
   at Castle.DynamicProxy.Contributors.MembersCollector.CollectProperties(IProxyGenerationHook hook)
   at Castle.DynamicProxy.Contributors.MembersCollector.CollectMembersToProxy(IProxyGenerationHook hook)
   at Castle.DynamicProxy.Contributors.ClassProxyWithTargetTargetContributor.CollectElementsToProxyInternal(IProxyGenerationHook hook)+MoveNext()
   at Castle.DynamicProxy.Contributors.CompositeTypeContributor.CollectElementsToProxy(IProxyGenerationHook hook, MetaType model)
   at Castle.DynamicProxy.Generators.ClassProxyWithTargetGenerator.GenerateType(String name, INamingScope namingScope)
   at Castle.DynamicProxy.Generators.BaseProxyGenerator.ObtainProxyType(CacheKey cacheKey, Func`3 factory)
   at Castle.DynamicProxy.ProxyGenerator.CreateClassProxyWithTarget(Type classToProxy, Type[] additionalInterfacesToProxy, Object target, ProxyGenerationOptions options, Object[] constructorArguments, IInterceptor[] interceptors)
   at ChangeTracking.Core.AsTrackable[T](T target, ChangeStatus status, Action`1 notifyParentListItemCanceled, Boolean makeComplexPropertiesTrackable, Boolean makeCollectionPropertiesTrackable)
   at ChangeTracking.ChangeTrackingCollectionInterceptor`1..ctor(IList`1 target, Boolean makeComplexPropertiesTrackable, Boolean makeCollectionPropertiesTrackable)
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
   at ChangeTracking.Core.AsTrackableCollectionChild(Type type, Object target, Boolean makeComplexPropertiesTrackable, Boolean makeCollectionPropertiesTrackable)
   at ChangeTracking.CollectionPropertyInterceptor`1.<>c__DisplayClass12_0.<GetGetterAction>b__0(IInvocation invocation, Dictionary`2 trackables, Boolean makeComplexPropertiesTrackable, Boolean makeCollectionPropertiesTrackable)
   at ChangeTracking.CollectionPropertyInterceptor`1.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at ChangeTracking.ComplexPropertyInterceptor`1.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at ChangeTracking.ChangeTrackingInterceptor`1.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at ChangeTracking.NotifyPropertyChangedInterceptor`1.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at Castle.Proxies.TestThingProxy.get_MyStrings()
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, Object[] index)
   at ChangeTracking.NotifyPropertyChangedInterceptor`1.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()

We had the same problem.
As far as i remember the problem is, that the string class is sealed.
Requirements and restrictions

We solved it by creating a new class to wrap the string.
For example:

public class TrackableString : INotifyPropertyChanged
{
    private string _value;
    public virtual string Value
    {
        get => _value;
        set
        {
            _value = value;
            OnPropertyChanged();
        }
    }

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

It would be great if this library supported lists of primitives, even if you just get a record of the old list and new list and if the list as a whole was modified - it doesn't have to tell you per-item what was added/removed/modified

This was already discussed in #4. Thanks for the feedback, I will keep it in mine.