How to obtain the attribute information of data changes in C ා - class

I. Preface

In normal development, when the user modifies the data, there is no good way to record the specific modified information. We can only temporarily use the method of serializing the class into a json string and then inserting it into the log. At this time, if we want to know which field values the user has changed, it is very difficult. Therefore, take advantage of this holiday to solve this small problem that has been left behind. This article records my current implementation method. If you have a different solution than the one listed in the article, please point out.

Code storage address: https://github.com/Lanesra712/ingos-common/tree/master/sample/csharp/get-data-changed-properties

 

2, Step by Step

1. Demand scenario

In A frequently encountered usage scenario, user A modifies the data information on A form page, and then submits it to our server to complete the data update. For users with certain permissions, it is expected to see the data changes before and after all users operate on the form.

2. Solutions

Since we want to know the data difference before and after the user's operation, we must compare the data before and after the user's operation, which falls to the class we undertake the data.

When we define the properties in a class, we use the automatic property method to complete the getter and setter declaration of the properties, while the complete property declaration method requires us to define a field to undertake the change of the property.

// Automatic property declaration
public class Entity1
{
    public Guid Id { get; set; }
}

// Full attribute declaration
public class Entity2
{
    private Guid _id;

    public Guid Id
    {
        get => _id;
        set => _id = value;
    }
}

Because when assigning a property, we need to call the set constructor of the property. Therefore, in the set constructor, we can directly judge the newly assigned value to record the change process of the property. The modified class property declaration code is as follows.

public class Sample
{
    private string _a;

    public string A
    {
        get => _a;
        set
        {
            if (_a == value)
                return;

            string old = _a;
            _a = value;
            propertyChangelogs.Add(new PropertyChangelog<Sample>(nameof(A), old, _a));
        }
    }

    private double _b;

    public double B
    {
        get => _b;
        set
        {
            if (_b == value)
                return;

            double old = _b;
            _b = value;
            propertyChangelogs.Add(new PropertyChangelog<Sample>(nameof(B), old.ToString(), _b.ToString()));
        }
    }

    private IList<PropertyChangelog<Sample>> propertyChangelogs = new List<PropertyChangelog<Sample>>();

    public IEnumerable<PropertyChangelog<Sample>> Changelogs() => propertyChangelogs;
}

In the modified class attribute declaration, we judge the newly assigned value and the original value in the set constructor of the attribute. When there are two different values, they are written into the set of change records, so as to achieve the purpose of recording data changes. Here, the entity class attribute of the change record is defined as follows.

public class PropertyChangelog<T>
{
    /// <summary>
    /// ctor
    /// </summary>
    public PropertyChangelog()
    { }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="propertyName">Attribute name</param>
    /// <param name="oldValue">Old value</param>
    /// <param name="newValue">New value</param>
    public PropertyChangelog(string propertyName, string oldValue, string newValue)
    {
        PropertyName = propertyName;
        OldValue = oldValue;
        NewValue = newValue;
    }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="className">Class name</param>
    /// <param name="propertyName">Attribute name</param>
    /// <param name="oldValue">Old value</param>
    /// <param name="newValue">New value</param>
    /// <param name="changedTime">Modification time</param>
    public PropertyChangelog(string className, string propertyName, string oldValue, string newValue, DateTime changedTime)
        : this(propertyName, oldValue, newValue)
    {
        ClassName = className;
        ChangedTime = changedTime;
    }

    /// <summary>
    /// Class name
    /// </summary>
    public string ClassName { get; set; } = typeof(T).FullName;

    /// <summary>
    /// Attribute name
    /// </summary>
    public string PropertyName { get; set; }

    /// <summary>
    /// Old value
    /// </summary>
    public string OldValue { get; set; }

    /// <summary>
    /// New value
    /// </summary>
    public string NewValue { get; set; }

    /// <summary>
    /// Modification time
    /// </summary>
    public DateTime ChangedTime { get; set; } = DateTime.Now;
}

As you can see, when we initialize the Sample class, we record the data change records about the class attributes twice. When we re assign the class attributes, only attribute A changes the data, so we only record the data change records of attribute A.

Although we have achieved our goal here, if we adopt this method, it is equivalent to that the attribute declaration methods of the classes that need to realize the data recording function in the original project need to be rewritten. At the same time, based on C #, we have provided the automatic attribute method to simplify the attribute declaration. As a result, now we return to the traditional attribute declaration method, which seems to be somewhat different Too smart. Therefore, since it is too cumbersome to compare attributes one by one, here we directly compare the two entity classes before and after modification by reflection to obtain attribute information of data changes in batches.

What we want to achieve finally is that users can see the process of the change of the field attribute data of a form, and sometimes the attributes we define in the C ා class need to be mapped with the field name displayed on the actual page, and some attributes do not need to record the change of data. Here I will improve the implementation of the function by adding custom features.

/// <summary>
/// Set the data change record for the specified attribute
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class PropertyChangeTrackingAttribute : Attribute
{
    /// <summary>
    /// Appoint PropertyChangeTrackingAttribute Default value for property
    /// </summary>
    public static readonly PropertyChangeTrackingAttribute Default = new PropertyChangeTrackingAttribute();

    /// <summary>
    /// Construct a new PropertyChangeTrackingAttribute Characteristic example
    /// </summary>
    public PropertyChangeTrackingAttribute()
    { }

    /// <summary>
    /// Construct a new PropertyChangeTrackingAttribute Characteristic example
    /// </summary>
    /// <param name="ignore">Whether to ignore the data change of this field</param>
    public PropertyChangeTrackingAttribute(bool ignore = false)
    {
        IgnoreValue = ignore;
    }

    /// <summary>
    /// Construct a new PropertyChangeTrackingAttribute Characteristic example
    /// </summary>
    /// <param name="displayName">Property corresponding page display name</param>
    public PropertyChangeTrackingAttribute(string displayName)
        : this(false)
        {
            DisplayNameValue = displayName;
        }

    /// <summary>
    /// Construct a new PropertyChangeTrackingAttribute Characteristic example
    /// </summary>
    /// <param name="displayName">Property corresponding page display name</param>
    /// <param name="ignore">Whether to ignore the data change of this field</param>
    public PropertyChangeTrackingAttribute(string displayName, bool ignore)
        : this(ignore)
        {
            DisplayNameValue = displayName;
        }

    /// <summary>
    /// Get the name parameter information on the property corresponding page in the property
    /// </summary>
    public virtual string DisplayName => DisplayNameValue;

    /// <summary>
    /// Get whether to ignore the data change parameter information of this field in the attribute
    /// </summary>
    public virtual bool Ignore => IgnoreValue;

    /// <summary>
    /// Modify the display name parameter value of the corresponding page of the property
    /// </summary>
    protected string DisplayNameValue { get; set; }

    /// <summary>
    /// Modify whether to ignore the data change of this field
    /// </summary>
    protected bool IgnoreValue { get; set; }
}

Considering that our class may contain a lot of attribute information, it will be very troublesome to add attributes one by one, so we can directly add this attribute to the class here. At the same time, we may exclude some properties in the class, or set the name of the property displayed in the page. Here, we can add properties separately for specific class properties.

After completing the custom feature, considering the convenience of our subsequent use, here I use the form of creating extension method to declare our function method. At the same time, I add the DisplayName property in the PropertyChangelog class to store the property corresponding to the name stored on the page. The final code is as follows.

/// <summary>
/// Get class property data change record
/// </summary>
/// <typeparam name="T">Listening class type</typeparam>
/// <param name="oldObj">Class with original value</param>
/// <param name="newObj">Class after changing property value</param>
/// <param name="propertyName">Specified property name</param>
/// <returns></returns>
public static IEnumerable<PropertyChangelog<T>> GetPropertyLogs<T>(this T oldObj, T newObj, string propertyName = null)
{
    IList<PropertyChangelog<T>> changelogs = new List<PropertyChangelog<T>>();

    // 1,Get the attribute information of the data change record to be added
    //
    IList<PropertyInfo> properties = new List<PropertyInfo>();

    // PropertyChangeTracking Type of property
    var attributeType = typeof(PropertyChangeTrackingAttribute);

    // Attribute information contained in the corresponding class
    var classProperties = typeof(T).GetProperties();

    // Get the property information of the change record to be added in the class
    //
    bool flag = Attribute.IsDefined(typeof(T), attributeType);

    foreach (var i in classProperties)
    {
        // Get the attribute information added by the current attribute
        var attributeInfo = (PropertyChangeTrackingAttribute)i.GetCustomAttribute(attributeType);

        // Class does not add an attribute, and the attribute does not add an attribute
        if (!flag && attributeInfo == null)
            continue;

        // Class to add an attribute that does not add an attribute
        if (flag && attributeInfo == null)
            properties.Add(i);

        // Whether or not a class adds properties, as long as the properties in the class add properties, and Ignore by false
        if (attributeInfo != null && !attributeInfo.Ignore)
            properties.Add(i);
    }

    // 2,Determine whether the specified attribute data has changed
    //
    foreach (var property in properties)
    {
        var oldValue = property.GetValue(oldObj) ?? "";
        var newValue = property.GetValue(newObj) ?? "";

        if (oldValue.Equals(newValue))
            continue;

        // Gets the name of the current property displayed on the page
        //
        var attributeInfo = (PropertyChangeTrackingAttribute)property.GetCustomAttribute(attributeType);
        string displayName = attributeInfo == null ? property.Name
            : attributeInfo.DisplayName;

        changelogs.Add(new PropertyChangelog<T>(property.Name, displayName, oldValue.ToString(), newValue.ToString()));
    }

    return string.IsNullOrEmpty(propertyName) ? changelogs
        : changelogs.Where(i => i.PropertyName.Equals(propertyName));
}

In the following test case, the Entity class only records the data changes of five attributes. We manually create two Entity class instances and change the corresponding attribute values of the two class instances. From the diagram we run, we can see that although the Id property values of the two class instances are different, we have manually ignored them, so we only display the change information of several properties we set.

[PropertyChangeTracking]
public class Entity
{
    [PropertyChangeTracking(ignore: true)]
    public Guid Id { get; set; }

    [PropertyChangeTracking(displayName: "Serial number")]
    public string OId { get; set; }

    [PropertyChangeTracking(displayName: "First field")]
    public string A { get; set; }

    public double B { get; set; }

    public bool C { get; set; }

    public DateTime Date { get; set; } = DateTime.Now;
}

 

Three, summary

This chapter is a solution to a problem I met in my work before. Although it is only a small problem, it is still of great significance for reference. It's a great honor to provide you with some help in daily development.

Tags: C# Attribute JSON github

Posted on Thu, 06 Feb 2020 23:54:52 -0800 by jprazen