Dynamics CRM Connect: Implementing Custom Field Reader and Value Accessor

Working on integration with Dynamics CRM implementation, in some cases we had to have something more than a simple field cope from CRM to Sitecore and we need to do some manipulations with data before saving it. For example, we have an address field in the CRM, but we want to store geographical coordinates instead. Solution below describes how we can do that.

First of all we need to implement a custom field reader with a custom field converter:

      

using Sitecore.DataExchange.DataAccess;

namespace Example
{
    public interface IBaseValueReader: IValueReader
    {
        string AttributeName { get; set; }
    }
}
  

AddressFieldReader:

      
using System;
using Sitecore.DataExchange.DataAccess;
using Sitecore.DataExchange.Repositories;

namespace Example
{
    public class AddressFieldReader : IBaseValueReader
    {
        public string AttributeName { get; set; }
        public IItemModelRepository Repository { get; private set; }
        public ExampleFieldReader(IItemModelRepository repository)
        {
            Repository = repository;
        }

        public CanReadResult CanRead(object source, DataAccessContext context)
        {
            return new CanReadResult()
            {
                CanReadValue = true
            };
        }

        public ReadResult Read(object source, DataAccessContext context)
        {
            object result = (object)null;

            var sourceObject = source as Microsoft.Xrm.Sdk.Entity;
            if (sourceObject != null &&  AttributeName != null)
            {
                object value;

                //Try to read attribute by the attribute name 

                if (sourceObject.Attributes.TryGetValue(AttributeName, out value))
                {

                    //Then you need to cast it to the address field type

                    var searchObject = value as Microsoft.Xrm.Sdk.Money; //It is example. You need to debug and see which type you exactly get. 

                    //Then you need to check if field is not null and have value.

                    if (searchObject != null && searchObject.Value != Decimal.Zero)
                    {
                        var fieldValue = searchObject.Value;


                        //IMPORTANT!!!
                        //IMPORTANT!!!
                        //IMPORTANT!!! Now, when you got a value of field, you can use any API to get Geo Latitude and Longitude. It can be some GeoAPI.


                        // Your code here:

                        var geoLocation = fieldValue; 

                        //Then you should return result:

                        return new ReadResult(DateTime.UtcNow)
                        {
                            WasValueRead = true,
                            ReadValue = geoLocation
                        };
                    }
                }

            }

            return new ReadResult(DateTime.UtcNow)
            {
                WasValueRead = true,
                ReadValue = String.Empty
            };
        }
    }
}
  

AddressFieldReaderConverter:

      
using Sitecore.DataExchange.DataAccess;
using Sitecore.DataExchange.Extensions;
using Sitecore.DataExchange.Repositories;
using Sitecore.Services.Core.Model;
using System;
using Sitecore.DataExchange.Converters;

namespace Example
{
    public class AddressFieldReaderConverter : BaseItemModelConverter<ItemModel, IValueReader> 
    {
        private AddressFieldReader _reader = (AddressFieldReader)null;


        private static readonly Guid TemplateId = Guid.Parse("{C3C9E93C-F197-4FBD-820D-DCB565707AF7}");
        public AddressFieldReaderConverter(IItemModelRepository repository)
      : base(repository)
        {
            this.SupportedTemplateIds.Add(TemplateId);
        }

        public override bool CanConvert(ItemModel source)
        {
            return true;
        }

        public override IValueReader Convert(ItemModel source)
        {
            if (source == null)
            {
                Sitecore.DataExchange.Context.Logger.Error("Cannot convert null item to value reader. (converter: {0})", (object)this.GetType().FullName);
                return (IValueReader)null;
            }
            if (!this.CanConvert(source))
            {
                Sitecore.DataExchange.Context.Logger.Error("Cannot convert item to value reader. (item: {0}, converter: {1})", (object)source.GetItemId(), (object)this.GetType().FullName);
                return (IValueReader)null;

            }

            if (_reader == null)
                _reader = new AddressFieldReader(ItemModelRepository);
            return (IValueReader)_reader;
        }
    }
}
  

You also need to implement a Value Accessor for passing the Attribute Name Value from the Value Accessor to the Field Reader:

      
using Sitecore.DataExchange.Converters.DataAccess.ValueAccessors;
using Sitecore.DataExchange.DataAccess;
using Sitecore.DataExchange.DataAccess.Writers;
using Sitecore.DataExchange.Providers.DynamicsCrm.DataAccess.Readers;
using Sitecore.DataExchange.Repositories;
using Sitecore.Services.Core.Model;
using System;

namespace Example
{
    public class CustomValueAccessorConverter : ValueAccessorConverter
    {
        private static readonly Guid TemplateId = Guid.Parse("{C3C9E93C-F197-4FBD-820D-DCB565707AF7}");

        public LookupValueAccessorConverter(IItemModelRepository repository)
          : base(repository)
        {
            this.SupportedTemplateIds.Add(LookupValueAccessorConverter.TemplateId);
        }

        public override IValueAccessor Convert(ItemModel source)
        {
            IValueAccessor valueAccessor = base.Convert(source);
            if (valueAccessor == null)
                return (IValueAccessor)null;


            string stringValue = this.GetStringValue(source, "AttributeName");
            if (string.IsNullOrWhiteSpace(stringValue))
                return (IValueAccessor)null;
            bool boolValue = this.GetBoolValue(source, "UseValueProperty");



            if (valueAccessor.ValueReader == null)
            {
                EntityAttributeValueReader attributeValueReader = new EntityAttributeValueReader(stringValue);
                if (this.GetBoolValue(source, "UseValueProperty"))
                    attributeValueReader.UseValueProperty = true;
                valueAccessor.ValueReader = (IValueReader)attributeValueReader;
            }
            else
            {
                //Custom part. Initialize attribute for read

                var customFieldReader = valueAccessor.ValueReader as IBaseValueReader;
                if (customFieldReader != null)
                {
                    customFieldReader.AttributeName = stringValue;
                }
            }

            if (valueAccessor.ValueWriter == null && !boolValue)
                valueAccessor.ValueWriter = (IValueWriter)new IndexerPropertyValueWriter(new object[1]
                {
          (object) stringValue
                });

            return valueAccessor;
        }
    }
}

  

Then you need to create a template for AddressField Reader and inherit it from the Value Reader template (/sitecore/templates/Data Exchange/Framework/Data Access/Value Readers/Value Reader):

crm connect template



crm connect template 2

Then, based on this template, you need to create a Value Reader Item (make sure the converter is correct):

CRM  connect Value Reader Item

The next step is to create a Value Accessor for your import object (make sure the Converter type set to your custom implemented):

crm connect Value Accessor

You can use this Value Accessor in your Value Mapping Set.

After that you need to run an import and see if it is works.

Did it work for you? Tell us what you think!