Showing posts with label Dynamics Migration. Show all posts
Showing posts with label Dynamics Migration. Show all posts

Monday, November 25, 2024

Migrating and Preserving ModifiedOn, ModifiedBy values of Dynamics 365/CRM

Out of the box there's no solution to keep the original source values of ModifiedOn and ModifiedBy when migrating data records to Dynamics 365. In an on-premises deployment you could accomplish the task by the unsupported query update on the destination database; but if you are migrating to cloud Dynamics 365 database access is not an option. In this article I'll walk you through step-by-step guide of one solution proven to work in migration projects. The migration scenario is to move data from an older Dynamics CRM system to online Dynamics 365/CRM; but this could work for other data sources wishing to migrate to Dynamics 365/CRM.

The steps are:
1. Create temporary fields in destination entity to hold the source values of ModifiedOn and ModifiedBy.
2. Write plug-in to set ModifiedOn/By values of destination entity.
3. Register the plug-in.
4. Map source fields to temporary fields and run the migration/import

Step 1: Create 2 temp fields new_ModifiedOn and new_ModifiedBy in the destination environment. Whatever method/tool of migration you will use, the source ModifiedOn will set value for destination new_ModifiedOn; and source ModifiedBy will set value for destination new_ModifiedBy.

Step 2: Write plug-in to set value of destination ModifiedOn and ModifiedBy. Core code for the plug-in is at the end.

Step 3: Register the plug-in, on create message and Pre-Operation.

Step 4: Map the fields for migration/import, like I mentioned in Step 1 - source ModifiedOn to destination new_ModifiedOn, and ModifiedBy to new_ModifiedBy

Now if you run the migration/import, the plug-in will pickup the original values in temp fields and set them for ModifiedOn/ModifiedBy upon creation of new record.

Here is my plugin:

using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace TXN.Plugin.PreserveRecordInfo
{
    public class PreCreateRecord : IPlugin
    {
        #region Secure/Unsecure Configuration Setup
        private string _secureConfig = null;
        private string _unsecureConfig = null;

        public PreCreateRecord(string unsecureConfig, string secureConfig)
        {
            _secureConfig = secureConfig;
            _unsecureConfig = unsecureConfig;
        }
        #endregion
        public void Execute(IServiceProvider serviceProvider)
        {
            ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = factory.CreateOrganizationService(context.UserId);

            if (context == null) throw new InvalidPluginExecutionException("IPluginExecutionContext is null");
            if (service == null) throw new InvalidPluginExecutionException("IOrganizationService is null");
            if (tracer == null) throw new InvalidPluginExecutionException("ITracingService is null");

            tracer.Trace("TXN: message: " + context.MessageName + " | Timestamp: " + DateTime.Now);
            
            if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
            {
                Entity entity = (Entity)context.InputParameters["Target"];

                string _message = context.MessageName.ToUpper();
                string _entityLogical = entity.LogicalName;

                tracer.Trace("TXN: entity: " + entity.LogicalName + " | GUID: " + entity.Id.ToString());
                //TXN: Filter the message and your entities here
                if (!(_message == "CREATE" ) ||
                    !(_entityLogical == "knowledgearticle" || 
                      _entityLogical == "adobe_agreement" ||
                      _entityLogical == "phonecall" ||
                      _entityLogical == "fax" ||
                      _entityLogical == "letter" ||
                      _entityLogical == "appointment" ||
                      _entityLogical == "email" ||
                      _entityLogical == "task"))
                    return;

                try
                {
                    DateTime _initDate = new DateTime(2099, 12, 12);
                    DateTime _modifiedOn = _initDate;
                    string _modifiedBy = null;

                    if (entity.Contains("new_modifiedon") && entity.Attributes["new_modifiedon"] != null)
                    {
                        _modifiedOn = (DateTime)entity.Attributes["new_modifiedon"];
                        tracer.Trace("TXN: mapped new_modifiedon: " + _modifiedOn.ToString());
                    }

                    if (entity.Contains("new_modifiedbyname") && entity.Attributes["new_modifiedbyname"] != null)
                    {
                        _modifiedBy = (entity.Attributes["new_modifiedbyname"]).ToString();
                        tracer.Trace("TXN: mapped new_modifiedbyname: " + _modifiedBy);
                    }

                    //TXN: Set original modifiedon value in destination record
                    if(!_modifiedOn.Equals(_initDate))
                    {
                        entity.Attributes["modifiedon"] = _modifiedOn;
                        tracer.Trace("TXN: set modifiedon to: " + _modifiedOn.ToString());
                    }

                    //TXN: find new_modifiedbyname and set that user to destination record's modifiedby
                    if (_modifiedBy != null)
                    {
                        var fetchXml = @"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
                              <entity name='systemuser'>
                                <attribute name='fullname' />
                                <attribute name='systemuserid' />
                                <filter type='and'>
                                  <condition attribute='fullname' operator='eq' value='{0}' />
                                </filter>
                              </entity>
                            </fetch>";
                        fetchXml = string.Format(fetchXml, _modifiedBy);
                        EntityCollection users = service.RetrieveMultiple(new FetchExpression(fetchXml));
                        if (users != null && users.Entities.Count > 0)
                        {
                            entity.Attributes["modifiedby"] = users.Entities[0].ToEntityReference();
                            tracer.Trace("TXN: set modifiedby to user: " + users.Entities[0].GetAttributeValue<string>("fullname"));
                        }
                    }
                }
                catch (Exception e)
                {
                    throw new InvalidPluginExecutionException(e.Message);
                }
            }
        }
    }
}

Wednesday, December 6, 2017

How to Create Disabled Users in Dynamics 365/CRM

This article discusses 2 possible options for creating inactive/disabled users within Dynamics 365/CRM. This type of users consume no license, and need not be associated with Active Directory, or Microsoft/Office 365 account, for authentication; and you would want to have them in your Dynamics CRM to, for example, hold data records migrated from another system.

Option #1 - Writing code.

At the minimum you have to set the following required attributes for a new disabled user:

       Entity _disabledUser = new Entity("systemuser");

       _disabledUser["firstname"] = "John";
       _disabledUser["lastname"] = "Smith";
       _disabledUser["issyncwithdirectory"] = false;
       _disabledUser["isdisabled"] = true;
       _disabledUser["internalemailaddress"] = "john.smith@fakemail.com";
       _disabledUser["businessunitid"] = new EntityReference("businessunit", new Guid("7AFD896E-B3AD-E711-A967-000D3A192828"));
       _disabledUser["domainname"] = "john.smith@fakedomain.com";

       _service.Create(_disabledUser);

Note that the phantom domainname (User Name) must be unique and not to duplicate with any other existing enabled or disabled users. The internalemailaddress is allowed to be duplicated value when I run the above code.

This option requires making program, and could be quick to generate a few users. But when the legacy users run in the hundreds, a better option is to use a software utility to read the legacy user list and generate them on the destination Dynamics system, with just a push of a button. And no coding needed. I'm talking about option 2 next.

Option #2 - Integration software.

If you've done some Dynamics data migration and integration, you might hear of Kingswaysoft SSIS Integration Toolkit for Microsoft Dynamics 365. The toolkit comes with components that allow you to connect to the source and destination Dynamics CRM sites to read and write data records. Of course all the standard ETL components are available for your SSIS data transformation project. The Toolkit is free if you don't want to deploy to run automatically on schedule, which is fine here since producing disabled users is not something to do daily.

Here's the snapshot of the data flow:























The Derived Columns component contains constant values for issyncwithdirectory, isdisabled, and businessunitid that will be used in the destination component for every disabled user record:
















The rest of required user data will come from the source. So the destination field mappings will look like this:



















Here I'm calling the Upsert action on the systemuser entity. Create action would be more optimal.


























Hope this helps with your Dynamics 365/CRM migration project. I'm sure there are other options. The SSIS Integration Toolkit from Kingswaysoft really helps jump start my project with so many legacy users in the old CRM system.