Introduction

In CRM systems, entity records serve as the central unit, and all system operations are based on manipulating these records of various entity types. Users often require information about who made specific changes to certain field values and when those changes occurred. While the auditing functionality provided by the system can fulfill this requirement, it can significantly increase the database's size over time. To address this issue, administrators may need to delete audit logs or disable auditing altogether for maintenance purposes. In such cases, custom auditing implementation becomes necessary to meet the customer's needs while avoiding unnecessary data. This article presents the implementation of a custom auditing logger for the Task entity.

Logger Implementation

The logger architecture consists of three modules that address three main questions:

1.    How to fetch auditing data?
2.    Where to store auditing data?
3.    How to visualize auditing data?

Let's explore the implementation of these modules.

Logger Fetcher

The Fetcher module is a standalone plugin registered with a single step on the Update Post-Operation Asynchronous stage. This step contains pre- and post-images that define the fields to be audited.


Important Note! The filtering attributes of the step, pre-image parameters, and post-image parameters must have the same set of attributes.

Plugin Manager validates all the necessary data and, after successful validation, launches the fetching process.

private void FillTaskHistory(LocalPluginContext localContext)
{
    IPluginExecutionContext context = localContext.PluginExecutionContext;
    if (context.PreEntityImages.TryGetValue("PreImage", out Entity preImage) && context.PostEntityImages.TryGetValue("PostImage", out Entity postImage))
    {
        new TaskService(localContext.SystemOrganizationService)
            .FillTaskHistory(context.PrimaryEntityId, context.InitiatingUserId, preImage, postImage);
    }
}

The next step is to filter audit field attributes based on their value-changing aspect.
1.    Attributes with removed values (old).

List<string> oldValueAttributes = preImage.Attributes.Keys.Except(postImage.Attributes.Keys).ToList();

2.    Attributes with a newly set values.

List<string> newValueAttributes = postImage.Attributes.Keys.Except(preImage.Attributes.Keys).ToList();

3.    Attributes with changed values.

List<string> changedValueAttributes = preImage.Attributes.Keys.Intersect(postImage.Attributes.Keys).ToList();

The fetch function is then executed for each attribute group, and the log model is added to the summary collection.
1.    Old attribute processing.

foreach (string attribute in oldValueAttributes)
{
    TaskLogModel log = getAttributeLog(preImage, "old", attribute, null);

    if (log != null)
    {
        logs.Add(log);
    }
}

2.    New attribute processing.

foreach (string attribute in newValueAttributes)
{
    TaskLogModel log = getAttributeLog(postImage, "new", attribute, null);

    if (log != null)
    {
        logs.Add(log);
    }
}

3.    Changed attribute processing.

foreach (string attribute in changedValueAttributes)
{
    TaskLogModel log = getAttributeLog(preImage, "changed", attribute, postImage);

    if (log != null)
    {
        logs.Add(log);
    }
}

Inside the getAttributeLog function, the attribute type is fetched using the RetrieveAttributeRequest message. Then, a log specific to that attribute type is created.

TaskLogModel getAttributeLog(Entity target, string type, string attribute, Entity compareTarget = null)
{
    TaskLogModel log = null;
    AttributeTypeCode? attributeType = metadataRepository.RetrieveAttributeType(EntityName, attribute);
    switch (attributeType)
    {
        case AttributeTypeCode.Memo:
        case AttributeTypeCode.String:
            {
                string value = target.GetAttributeValue<string>(attribute);

                if (type == "old")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value,
                        NewValue = string.Empty
                    };
                }
                else if (type == "new")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = string.Empty,
                        NewValue = value,
                    };
                }
                else if (compareTarget != null && value != compareTarget.GetAttributeValue<string>(attribute))
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value,
                        NewValue = compareTarget.GetAttributeValue<string>(attribute)
                    };
                }
            }
            break;
        case AttributeTypeCode.Boolean:
            {
                bool? value = target.GetAttributeValue<bool?>(attribute);

                if (type == "old")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = attributeType.ToString(),
                        OldValue = value == true ? "true" : "false",
                        NewValue = string.Empty,
                    };
                }
                else if (type == "new")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = attributeType.ToString(),
                        OldValue = string.Empty,
                        NewValue = value == true ? "true" : "false",
                    };
                }
                else if (compareTarget != null && value != compareTarget.GetAttributeValue<bool?>(attribute))
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = attributeType.ToString(),
                        OldValue = value == true ? "true" : "false",
                        NewValue = compareTarget.GetAttributeValue<bool?>(attribute) == true ? "true" : "false",
                    };
                }
            }
            break;
        case AttributeTypeCode.Integer:
            {
                int? value = target.GetAttributeValue<int?>(attribute);

                if (type == "old")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value?.ToString() ?? string.Empty,
                        NewValue = string.Empty,
                    };
                }
                else if (type == "new")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = string.Empty,
                        NewValue = value?.ToString() ?? string.Empty,
                    };
                }
                else if (compareTarget != null && value != compareTarget.GetAttributeValue<int?>(attribute))
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value?.ToString() ?? string.Empty,
                        NewValue = compareTarget.GetAttributeValue<int?>(attribute)?.ToString() ?? string.Empty
                    };
                }
            }
            break;
        case AttributeTypeCode.Double:
            {
                double? value = target.GetAttributeValue<double?>(attribute);

                if (type == "old")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
                        NewValue = string.Empty,
                    };
                }
                else if (type == "new")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = string.Empty,
                        NewValue = value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
                    };
                }
                else if (compareTarget != null && value != compareTarget.GetAttributeValue<double?>(attribute))
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
                        NewValue = compareTarget.GetAttributeValue<int?>(attribute)?.ToString(CultureInfo.InvariantCulture) ?? string.Empty
                    };
                }
            }
            break;
        case AttributeTypeCode.Decimal:
            {
                decimal? value = target.GetAttributeValue<decimal?>(attribute);

                if (type == "old")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
                        NewValue = string.Empty,
                    };
                }
                else if (type == "new")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = string.Empty,
                        NewValue = value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
                    };
                }
                else if (compareTarget != null && value != compareTarget.GetAttributeValue<decimal?>(attribute))
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
                        NewValue = compareTarget.GetAttributeValue<int?>(attribute)?.ToString(CultureInfo.InvariantCulture) ?? string.Empty
                    };
                }
            }
            break;
        case AttributeTypeCode.Owner:
        case AttributeTypeCode.Lookup:
            {
                EntityReference value = target.GetAttributeValue<EntityReference>(attribute);

                if (type == "old")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = AttributeTypeCode.Lookup.ToString(),
                        OldValue = value != null
                            ? JsonRepository.GetJSONFromAnonymousObject(TaskManagementRepository.ToLookupItemModel(value))
                            : string.Empty,
                        NewValue = string.Empty,
                    };
                }
                else if (type == "new")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = AttributeTypeCode.Lookup.ToString(),
                        OldValue = string.Empty,
                        NewValue = value != null
                            ? JsonRepository.GetJSONFromAnonymousObject(TaskManagementRepository.ToLookupItemModel(value))
                            : string.Empty,
                    };
                }
                else if (compareTarget != null && value?.Id != compareTarget.GetAttributeValue<EntityReference>(attribute)?.Id)
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = AttributeTypeCode.Lookup.ToString(),
                        OldValue = value != null
                            ? JsonRepository.GetJSONFromAnonymousObject(TaskManagementRepository.ToLookupItemModel(value))
                            : string.Empty,
                        NewValue = compareTarget.GetAttributeValue<EntityReference>(attribute) != null
                            ? JsonRepository.GetJSONFromAnonymousObject(TaskManagementRepository.ToLookupItemModel(compareTarget.GetAttributeValue<EntityReference>(attribute)))
                            : string.Empty,
                    };
                }
            }
            break;
        case AttributeTypeCode.State:
        case AttributeTypeCode.Status:
        case AttributeTypeCode.Picklist:
            {
                OptionSetValue value = target.GetAttributeValue<OptionSetValue>(attribute);

                if (type == "old")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = attributeType.ToString(),
                        OldValue = value?.Value.ToString() ?? string.Empty,
                        NewValue = string.Empty,
                    };
                }
                else if (type == "new")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = attributeType.ToString(),
                        OldValue = string.Empty,
                        NewValue = value?.Value.ToString() ?? string.Empty,
                    };
                }
                else if (compareTarget != null && value?.Value != compareTarget.GetAttributeValue<OptionSetValue>(attribute)?.Value)
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        Type = attributeType.ToString(),
                        OldValue = value?.Value.ToString() ?? string.Empty,
                        NewValue = compareTarget.GetAttributeValue<OptionSetValue>(attribute)?.Value.ToString() ?? string.Empty,
                    };
                }
            }
            break;
        case AttributeTypeCode.DateTime:
            {
                DateTime? value = target.GetAttributeValue<DateTime?>(attribute)?.ToLocalTime().Date;

                if (type == "old")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value?.ToString("d.M.yyyy") ?? string.Empty,
                        NewValue = string.Empty,
                    };
                }
                else if (type == "new")
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = string.Empty,
                        NewValue = value?.ToString("d.M.yyyy") ?? string.Empty,
                    };
                }
                else if (compareTarget != null && value != compareTarget.GetAttributeValue<DateTime?>(attribute)?.ToLocalTime().Date)
                {
                    log = new TaskLogModel
                    {
                        Attribute = attribute,
                        OldValue = value?.ToString("d.M.yyyy") ?? string.Empty,
                        NewValue = compareTarget.GetAttributeValue<DateTime?>(attribute)?.ToLocalTime().Date.ToString("d.M.yyyy") ?? string.Empty
                    };
                }
            }
            break;
    }
    return log;
}

The log model stores values in string format. For complex objects, we use transformation into JSON notation.

Logger Store

To store the log information, a technical entity named "Task Log" was created with a minimal set of fields. These include a lookup to the changed record (Task), the timestamp of the changes (Changed), a lookup to the user responsible for the changes (Changed By), and a multiline string field that contains all changes in JSON format (Changes, Size = 1,048,576 characters).

The Logger Fetcher captures all changes made in a single transaction, converts them to JSON format, and creates a log record.

string json = JsonRepository.GetJSONFromObject(logs.Where(x => x.Attribute != "modifiedon"));
taskLogRepository.CreateTaskLog(new AttributeCollection
{
    {"uds_name", taskId.ToString()},
    {"uds_taskid", new EntityReference(EntityName, taskId)},
    {"uds_changedon", postImage.GetAttributeValue<DateTime?>("modifiedon") ?? DateTime.UtcNow},
    {"uds_changedby", new EntityReference("systemuser", userId)},
    {"uds_changes", json}
});

Logger Visualizer

The Task log information is utilized in a sophisticated UI tool based on ReactJS + TypeScript for managing tasks in Dynamics. The log data visualization is accomplished using the Table component from the MUI component library.

Summary

In this article, we showcased a personalized logger that monitors modifications to entity record fields. We emphasized the limitations of using the standard auditing feature in CRM, which could result in excessive database growth. To address this concern, we recommended a custom auditing approach that is tailored to your specific requirements and minimizes needless data storage. 

Don't hesitate to reach out if you need any assistance. Our team is always available to help.