Quantcast
Channel: Field wrappers for tracking and applying changes to database fields - Code Review Stack Exchange
Viewing all articles
Browse latest Browse all 2

Field wrappers for tracking and applying changes to database fields

$
0
0

[This is a repost with entirely new wording and some minor changes in the code, not a duplicate of my previous question which has been deleted.]

Using Linq2DB I have a record class similar the following (cut down for example only):

[Table(Name = "Contacts")]public class Contact{    [PrimaryKey, Identity]    public int ID { get; set; }    [Column, NotNull]     public string Name { get; set }    [Column, Nullable]    public string Email { get; set; }    [Column, Nullable]    public DateTime? LastContact { get; set; }}

I have a model class that wraps an instance of the above and provides write access to only the fields that can be changed, and tracks changes as they are written. The model class for the above is:

public class ContactModel{    private static TestDB DB => Databases.GetDB<TestDB>();    private Contact _contact;    public int ID => _contact?.ID ?? -1;    private bool _nameChanged = false;    private bool _emailChanged = false;    private bool _lastcontactChanged = false;    private string _nameCurrent = null;    private string _emailCurrent = null;    private DateTime? _lastcontactCurrent = null;    public string Name    {        get => _nameChanged ? _nameCurrent : _contact?.Name;        set        {            _nameChanged = string.Compare(value, _contact?.Name) != 0;            _nameCurrent = _nameChanged ? value : null;        }    }    public string Email    {        get => _emailChanged ? _emailCurrent : _contact?.Email;        set        {            _emailChanged = string.Compare(value, _contact?.Email) != 0;            _emailCurrent = _emailChanged ? value : null;        }    }    public DateTime? LastContact    {        get => _lastcontactChanged ? _lastcontactCurrent : _contact?.LastContact;        set        {            _lastcontactChanged = value != _contact?.LastContact;            _lastcontactCurrent = _lastcontactChanged ? value : null;        }    }    private ContactModel(Contact contact)    {        _contact = contact;    }    [IgnoreDataMember]    public bool Changed => _nameChanged || _emailChanged || _lastcontactChanged;    public void Reset()    {        _nameChanged = _emailChanged = _lastcontactChanged = false;        _nameCurrent = null;        _emailCurrent = null;        _lastcontactCurrent = null;    }    public bool Update()    {        if (!Changed)            return true;        try        {            var db = DB;            int id = ID;            if (_contact == null)            {                id = db.Contacts.InsertWithInt32Identity(() =>                        new Contact                        {                            Name = Name,                            Email = Email,                            LastContact = LastContact                        }                    );            }            else            {                var upd = db.Contacts.Where(c => c.ID == id).AsUpdatable();                if (_nameChanged)                    upd = upd.Set(_ => _.Name, Name);                if (_emailChanged)                    upd = upd.Set(_ => _.Email, Email);                if (_lastcontactChanged)                    upd = upd.Set(_ => _.LastContact, LastContact);                if (upd.Update() < 1)                    return false;            }            _contact = db.Contacts.Single(c => c.ID == id);            Reset();            return true;        }        catch        { }        return false;    }    public static ContactModel LoadContact(int id, bool create = true)    {        var contact = DB.Contacts.FirstOrDefault(c => c.ID == id);        return (!create && contact != null) ? null : new ContactModel(contact);    }    public static ContactModel NewContact()    {        return new ContactModel(null);    }    public static ContactModel LoadContactByName(string name, bool create = false)    {        var contact = DB.Contacts.FirstOrDefault(c => c.Name == name);        if (contact == null)        {            if (!create)                return null;            contact = new Contact { Name = name };        }        return new ContactModel(contact);    }}

Initially I was writing essentially the same code multiple times to manage the changes, and of course my records tend to have many more fields than the above. After missing a field in the Changed property in one of my models caused me to pull out some of my hair trying to figure out why it wouldn't post updates to the database I decided to simplify things.

Since I have uses for this in a variety of places I've split the base class into two parts:

  • Editable<TValue> - tracks changes, usable in several places
  • RecordEditable<TRecord, TValue> - can apply changes via the ORM I'm using, only works with that ORM.

The Editable<> base class has the following basic properties:

  • Accesses the source value via a Func<TValue> so I don't have to reinitialize when the record is reloaded.
  • Tracks changes on assignment.
  • Can be reset to

This is my current implementation:

/// <summary>Interface for object that can be edited</summary>public interface IEditable : IDisposable{    /// <summary>Display name of the changeable object</summary>    string Name { get; }    /// <summary>True if current and source values differ</summary>    bool Changed { get; }    /// <summary>Reset to source value</summary>    void Reset();}/// <summary>Interface for editable values of a specific type.</summary>/// <typeparam name="TValue">Type of contained value</typeparam>public interface IEditable<TValue> : IEditable{    /// <summary>Source value before changes</summary>    TValue SourceValue { get; }    /// <summary>Current value: source or new value if changed</summary>    TValue Value { get; set; }}/// <summary>Implement change tracking for a value</summary>/// <typeparam name="TValue">Type of value to manage</typeparam>public class Editable<TValue> : IEditable<TValue>{    // Always uses the default comparer for the value type    protected static Comparer<TValue> _comparer = Comparer<TValue>.Default;    // Function that fetches the source value    private Func<TValue> _getSource;    private string _name = null;    /// <summary>Display name of editable object</summary>    public virtual string Name    {        get => _name;        set => _name = value;    }    private bool _changed = false;    /// <summary>True if assigned a value different to source</summary>    public bool Changed => _changed;    /// <summary>Source value</summary>    public TValue SourceValue => _getSource == null ? default : _getSource();    private TValue _value = default;    /// <summary>Current value</summary>    public TValue Value    {        get => _changed ? _value : SourceValue;        set        {            _changed = _comparer.Compare(value, SourceValue) != 0;            _value = _changed ? value : default;        }    }    /// <summary>Constructor</summary>    /// <param name="getSource">Function to return the source value</param>    public Editable(Func<TValue> getSource)    {        _getSource = getSource;    }    /// <summary>IDisposable implementation</summary>    public void Dispose()        => Dispose(true);    /// <summary>Release all references, etc</summary>    /// <param name="disposing">True if disposing, false if finalizing</param>    protected virtual void Dispose(bool disposing)    {        if (disposing)        {            _getSource = null;            _changed = false;            _value = default;        }    }    /// <summary>Clear any changes and reset to source value</summary>    public void Reset()    {        _changed = false;        _value = default;    }}

And this is a sample model class that uses the Editable type:

public class ContactModel : IDisposable{    private static TestDB DB => Databases.GetDB<TestDB>();    private Contact _contact;    public int ID => _contact?.ID ?? -1;    private bool _nameChanged = false;    private bool _emailChanged = false;    private bool _lastcontactChanged = false;    private string _nameCurrent = null;    private string _emailCurrent = null;    private DateTime? _lastcontactCurrent = null;    public string Name    {        get => _nameChanged ? _nameCurrent : _contact?.Name;        set        {            _nameChanged = string.Compare(value, _contact?.Name) != 0;            _nameCurrent = _nameChanged ? value : null;        }    }    public string Email    {        get => _emailChanged ? _emailCurrent : _contact?.Email;        set        {            _emailChanged = string.Compare(value, _contact?.Email) != 0;            _emailCurrent = _emailChanged ? value : null;        }    }    public DateTime? LastContact    {        get => _lastcontactChanged ? _lastcontactCurrent : _contact?.LastContact;        set        {            _lastcontactChanged = value != _contact?.LastContact;            _lastcontactCurrent = _lastcontactChanged ? value : null;        }    }    private ContactModel(Contact contact)    {        _contact = contact;    }    public void Dispose()        => Dispose(true);    protected virtual void Dispose(bool disposing)    {        _contact = null;        Reset();    }    [IgnoreDataMember]    public bool Changed => _nameChanged || _emailChanged || _lastcontactChanged;    public void Reset()    {        _nameChanged = _emailChanged = _lastcontactChanged = false;        _nameCurrent = null;        _emailCurrent = null;        _lastcontactCurrent = null;    }    public bool Update()    {        if (!Changed)            return true;        try        {            var db = DB;            int id = ID;            if (_contact == null)            {                id = db.Contacts.InsertWithInt32Identity(() =>                        new Contact                        {                            Name = Name,                            Email = Email,                            LastContact = LastContact                        }                    );            }            else            {                var upd = db.Contacts.Where(c => c.ID == id).AsUpdatable();                if (_nameChanged)                    upd = upd.Set(_ => _.Name, Name);                if (_emailChanged)                    upd = upd.Set(_ => _.Email, Email);                if (_lastcontactChanged)                    upd = upd.Set(_ => _.LastContact, LastContact);                if (upd.Update() < 1)                    return false;            }            _contact = db.Contacts.Single(c => c.ID == id);            Reset();            return true;        }        catch        { }        return false;    }    public static ContactModel LoadContact(int id, bool create = true)    {        var contact = DB.Contacts.FirstOrDefault(c => c.ID == id);        return (!create && contact != null) ? null : new ContactModel(contact);    }    public static ContactModel NewContact()    {        return new ContactModel(null);    }    public static ContactModel LoadContactByName(string name, bool create = false)    {        var contact = DB.Contacts.FirstOrDefault(c => c.Name == name);        if (contact == null)        {            if (!create)                return null;            contact = new Contact { Name = name };        }        return new ContactModel(contact);    }}

The Update method still requires writing the tests out in full, which is where the RecordUpdatable<> type comes in.

The IUpdatable.Set extension method uses LINQ Expressions to specify the field that is being updated. With a bit of expression manipulation I can create the getSource function from the field selector expression, with appropriate null checks and such.

Expression modification uses the following expression visitor:

public class ReplaceVisitor : ExpressionVisitor{    private Expression _from, _to;    public ReplaceVisitor(Expression from, Expression to)    {        _from = from;        _to = to;    }    public override Expression Visit(Expression node)        => node == _from ? _to : base.Visit(node);    public static T Transform<T>(T target, Expression from, Expression to)        where T : Expression        => (T)(new ReplaceVisitor(from, to).Visit(target));    // returns expression in form: () => record != null ? record.field : default(TValue);    public static Expression<Func<TValue>> SelectorToRecordGuarded<TRecord, TValue>(Expression<Func<TRecord, TValue>> fieldSelector, Expression<Func<TRecord>> fetchRecord)        => Expression.Lambda<Func<TValue>>(                Expression.Condition(                    Expression.NotEqual(Expression.Constant(null), fetchRecord.Body),                    Transform(fieldSelector.Body, fieldSelector.Parameters[0], fetchRecord.Body),                    Expression.Default(typeof(TValue))                )            );}

The interface and code below implement the database row update:

/// <summary>Interface for objects that can apply their changes to a compatible record</summary>/// <typeparam name="TRecord">Type of record this applies to</typeparam>public interface IApplyable<TRecord> : IEditable{    /// <summary>Applies any change to the supplied <see cref="IUpdatable{T}"/>.</summary>    /// <param name="updatable">Linq2DB <see cref="IUpdatable{T}"/> instance of appropriate record type</param>    /// <returns>New <see cref="IUpdatable{T}"/> instance with change applied, or <paramref name="updatable"/> if no change.</returns>    IUpdatable<TRecord> Apply(IUpdatable<TRecord> updatable);    /// <summary>A lambda expression that selects the field in the record, in the form: <code>r =&gt; r.field;</code></summary>    LambdaExpression FieldSelector { get; }}/// <summary>Implement change tracking for a field of a specific record type</summary>/// <typeparam name="TRecord">Type of record</typeparam>/// <typeparam name="TValue">Type of field</typeparam>public class RecordEditable<TRecord, TValue> : Editable<TValue>, IApplyable<TRecord>{    // Expression that selects a field from the record    // In the form: rec => rec.field;    private Expression<Func<TRecord, TValue>> _fieldSelector;    /// <summary>Display name, use field name if no name specified</summary>    public override string Name    {        get        {            string res = base.Name;            if (res == null)            {                // get field name from field selector expression                if (_fieldSelector.Body is MemberExpression m)                {                    res = base.Name = m.Member.Name;                }            }            return res;        }    }    // IApplyable implementation: strips type details from the actual field selector    LambdaExpression IApplyable<TRecord>.FieldSelector => _fieldSelector;    /// <summary>Construct from expressions</summary>    /// <param name="fetchRecord">Expression that returns the record containing the field</param>    /// <param name="fieldSelector">Expression that selects the field from the record</param>    public RecordEditable(Expression<Func<TRecord>> fetchRecord, Expression<Func<TRecord, TValue>> fieldSelector)        : base(ReplaceVisitor.SelectorToRecordGuarded(fieldSelector, fetchRecord).Compile())    {        _fieldSelector = fieldSelector;    }    // Clear references held by the field selector expression    protected override void Dispose(bool disposing)    {        if (disposing)        {            _fieldSelector = null;        }        base.Dispose(disposing);    }    /// <summary>If changed, apply the change to the supplied <see cref="IUpdatable{T}"/> </summary>    /// <param name="updatable">Linq2DB <see cref="IUpdatable{T}"/> instance of appropriate record type</param>    /// <returns>New <see cref="IUpdatable{T}"/> instance with change applied, or <paramref name="updatable"/> if no change.</returns>    public IUpdatable<TRecord> Apply(IUpdatable<TRecord> updatable)        => Changed ? updatable.Set(_fieldSelector, Value) : updatable;}

And the new model for the Contact class:

public class ContactModel2 : IDisposable{    private static TestDB DB => Databases.GetDB<TestDB>();    private Contact _contact;    private IEditable<string> _name;    private IEditable<string> _email;    private IEditable<DateTime?> _lastcontact;    public int ID => _contact?.ID ?? -1;    public string Name { get => _name.Value; set => _name.Value = value; }    public string Email { get => _email.Value; set => _email.Value = value; }    public DateTime? LastContact { get => _lastcontact.Value; set => _lastcontact.Value = value; }    private IEditable[] _fields;    private IEditable[] Fields    {        get        {            if (_fields == null)                _fields = new IEditable[] { _name, _email, _lastcontact };            return _fields;        }    }    private IEditable<TValue> Editable<TValue>(Expression<Func<Contact, TValue>> selector)        => new RecordEditable<Contact, TValue>(() => _contact, selector);    protected ContactModel2(Contact contact)    {        _contact = contact;        _name = Editable(_ => _.Name);        _email = Editable(_ => _.Email);        _lastcontact = Editable(_ => _.LastContact);    }    public void Dispose()        => Dispose(true);    protected virtual void Dispose(bool disposing)    {        if (disposing)        {            foreach (var field in Fields)                field.Dispose();            _contact = null;            _fields = null;            _name = null;            _email = null;            _lastcontact = null;        }    }    [IgnoreDataMember]    public bool Changed => Fields.Any(f => f.Changed);    public void Reset()    {        foreach (var f in Fields)            f.Reset();    }    public bool Update()    {        if (!Changed)            return true;        try        {            var db = DB;            int id = ID;            if (_contact == null)            {                id = db.Contacts.InsertWithInt32Identity(() =>                        new Contact                        {                            Name = Name,                            Email = Email,                            LastContact = LastContact                        }                    );            }            else            {                var upd = db.Contacts.Where(c => c.ID == ID).AsUpdatable();                foreach (var applyable in Fields.OfType<IApplyable<Contact>>())                    upd = applyable.Apply(upd);                if (upd.Update() < 1)                    return false;            }            _contact = db.Contacts.Single(c => c.ID == id);            Reset();            return true;        }        catch { }        return false;    }    public static ContactModel2 LoadContact(int id, bool create = true)    {        var contact = DB.Contacts.FirstOrDefault(c => c.ID == id);        return (!create && contact != null) ? null : new ContactModel2(contact);    }    public static ContactModel2 NewContact()    {        return new ContactModel2(null);    }    public static ContactModel2 LoadContactByName(string name, bool create = false)    {        var contact = DB.Contacts.FirstOrDefault(c => c.Name == name);        if (contact == null)        {            if (!create)                return null;            contact = new Contact { Name = name };        }        return new ContactModel2(contact);    }}

The above works, as far as it goes. When I have a lot of fields there's still a bit of slack, but I can move the bulk of the model code into a base class to further ease the repetition and reduce the likelihood of missing a field. I'd like to get this part improved a little before I focus on that, because there's a lot more expression juggling to get that working just right.

Question is, what can I do to improve on this? I'm not convinced it's the best code I can write for the problem, but I do feel like it's an improvement over the typing, and I'd rather not just write a code generator.


Viewing all articles
Browse latest Browse all 2

Latest Images

Trending Articles





Latest Images