Dynamics CRM: Effective Way Calling Custom Web API In Plugin Development

After I post several posts about Custom Web API (Dynamics CRM: Simplify Development with Custom API for Reusability and Get Environment Variable in a Flow using Custom Web API). You should be more clear about how to create the Custom Web API in your environment. Today, we will cover how to implement this feature effectively (real-world scenario) on the Plugin Development (backend).

For today’s demonstration, I already have a Custom API (all my custom API collection I put in here) in my environment with the name of “ins_getteamsemail”. This Custom API will return the array string of the email/userid and receives 2 parameters: boolean “IsEmail” and string “TeamName“. If we want to receive email, we can set IsEmail as true and TeamName we can put any string on it. If we want to call it in the normal way, we can use this code below:

using Microsoft.Xrm.Sdk;
using System;

namespace Blog.Plugin
{
    public abstract class PluginBase : IPlugin
    {
        protected string PluginClassName { get; }

        internal PluginBase(Type pluginClassName)
        {
            PluginClassName = pluginClassName.ToString();
        }

        public void Execute(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new InvalidPluginExecutionException("serviceProvider");
            }

            var localPluginContext = new LocalPluginContext(serviceProvider);

            localPluginContext.Trace($"Entered {PluginClassName}.Execute() " +
                $"Correlation Id: {localPluginContext.PluginExecutionContext.CorrelationId}, " +
                $"Initiating User: {localPluginContext.PluginExecutionContext.InitiatingUserId}");

            try
            {
                ExecuteCdsPlugin(localPluginContext);
                return;
            }
            catch (FaultException<OrganizationServiceFault> orgServiceFault)
            {
                localPluginContext.Trace($"Exception: {orgServiceFault.ToString()}");

                throw new InvalidPluginExecutionException($"OrganizationServiceFault: {orgServiceFault.Message}", orgServiceFault);
            }
            finally
            {
                localPluginContext.Trace($"Exiting {PluginClassName}.Execute()");
            }
        }

        protected virtual void ExecuteCdsPlugin(ILocalPluginContext localPluginContext)
        {
        }
    }

    public interface ILocalPluginContext
    {
        IOrganizationService CurrentUserService { get; }
        IOrganizationService SystemUserService { get; }
        IPluginExecutionContext PluginExecutionContext { get; }
        IServiceEndpointNotificationService NotificationService { get; }ITracingService TracingService { get; }
        void Trace(string message);
    }

    public class LocalPluginContext : ILocalPluginContext
    {
        public IOrganizationService CurrentUserService { get; }

        public IOrganizationService SystemUserService { get; }

        public IPluginExecutionContext PluginExecutionContext { get; }

        public IServiceEndpointNotificationService NotificationService { get; }

        public ITracingService TracingService { get; }

        public LocalPluginContext(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new InvalidPluginExecutionException("serviceProvider");
            }

            PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            TracingService = new LocalTracingService(serviceProvider);

            NotificationService = (IServiceEndpointNotificationService)serviceProvider.GetService(typeof(IServiceEndpointNotificationService));

            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

            CurrentUserService = factory.CreateOrganizationService(PluginExecutionContext.UserId);

            SystemUserService = factory.CreateOrganizationService(null);
        }

        public void Trace(string message)
        {
            if (string.IsNullOrWhiteSpace(message) || TracingService == null)
            {
                return;
            }

            TracingService.Trace(message);
        }
    }
    
    public class LocalTracingService : ITracingService
    {
        private readonly ITracingService _tracingService;

        private DateTime _previousTraceTime;

        public LocalTracingService(IServiceProvider serviceProvider)
        {
            DateTime utcNow = DateTime.UtcNow;

            var context = (IExecutionContext)serviceProvider.GetService(typeof(IExecutionContext));

            DateTime initialTimestamp = context.OperationCreatedOn;

            if (initialTimestamp > utcNow)
            {
                initialTimestamp = utcNow;
            }

            _tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

            _previousTraceTime = initialTimestamp;
        }

        public void Trace(string message, params object[] args)
        {
            var utcNow = DateTime.UtcNow;

            // The duration since the last trace.
            var deltaMilliseconds = utcNow.Subtract(_previousTraceTime).TotalMilliseconds;

            _tracingService.Trace($"[+{deltaMilliseconds:N0}ms)] - {message}");

            _previousTraceTime = utcNow;
        }
    }
    
    public class PreCacheOperation : PluginBase
    {
        public PreCacheOperation(string unsecureConfiguration, string secureConfiguration)
            : base(typeof(PreCacheOperation))
        {
        }

        protected override void ExecuteCdsPlugin(ILocalPluginContext localPluginContext)
        {
            if (localPluginContext == null)
            {
                throw new ArgumentNullException("localPluginContext");
            }

            IPluginExecutionContext context = localPluginContext.PluginExecutionContext;
         
            Entity entity = (Entity)context.InputParameters["Target"];
            var req = new OrganizationRequest("ins_getteamsemail")
            {
                ["IsEmail"] = true,
                ["TeamName"] = "Blog Team"
            };
            var team =
                localPluginContext.CurrentUserService.Execute(req);
            entity["ins_description"] = string.Join(",", team.Results["Result"] as string[]);
        }
    }
}

Here is the result from the screen:

System will set ins_description with all the email that is in Blog Team

The problem of the implementation above is about efficiency and reusability (same with the late-bound, we must remember the parameter name which is tedious to remember it). Let’s say we build a Custom Web API that is being used a lot in our project, we suppose have the collection of the Requests and Responses so the Developer will be easier to implement it in the code. Here is my attempt to enhance the above code:

using Microsoft.Xrm.Sdk;
using System;
using System.Runtime.Serialization;

namespace Blog.Plugin
{
    public static class OrganizationResponseExtensions
    {
        public static T Cast<T>(this Microsoft.Xrm.Sdk.OrganizationResponse response)
            where T : OrganizationResponse
        {
            var newClass = Activator.CreateInstance<T>();
            newClass.Results = response.Results;

            return newClass;
        }
    }

    [DataContract(Name = "OrganizationRequest",
        Namespace = "http://schemas.microsoft.com/xrm/2011/Contracts")]
    public sealed class GetTeamsEmailRequest : OrganizationRequest
    {
        public string TeamName
        {
            get
            {
                if (Parameters.Contains("TeamName"))
                {
                    return Parameters["TeamName"].ToString();
                }
                return "";
            }
            set { this["TeamName"] = value; }
        }

        public bool IsEmail
        {
            get
            {
                if (Parameters.Contains("IsEmail"))
                {
                    return bool.Parse(Parameters["IsEmail"].ToString());
                }
                return false;
            }
            set { this["IsEmail"] = value; }
        }

        public GetTeamsEmailRequest()
        {
            RequestName = "ins_getteamsemail";
        }
    }

    [DataContract(Name = "OrganizationResponse",
        Namespace = "http://schemas.microsoft.com/xrm/2011/Contracts")]
    public sealed class GetTeamsEmailResponse : Microsoft.Xrm.Sdk.OrganizationResponse
    {
        public string[] Emails => this["Result"] as string[];
    }
}

And here is the code when we want to implement it:

using Microsoft.Xrm.Sdk;
using System;
using System.Runtime.Serialization;

namespace Blog.Plugin
{
    public class PreCacheOperation : PluginBase
    {
        public PreCacheOperation(string unsecureConfiguration, string secureConfiguration)
            : base(typeof(PreCacheOperation))
        {
        }

        protected override void ExecuteCdsPlugin(ILocalPluginContext localPluginContext)
        {
            if (localPluginContext == null)
            {
                throw new ArgumentNullException("localPluginContext");
            }

            IPluginExecutionContext context = localPluginContext.PluginExecutionContext;

            Entity entity = (Entity) context.InputParameters["Target"];
            var req = new GetTeamsEmailRequest {IsEmail = true, TeamName = "Blog Team"};
            var team = localPluginContext.CurrentUserService.Execute(req).Cast<GetTeamsEmailResponse>();
            entity["ins_description"] = string.Join(",", team.Emails);
        }
    }
}

If you analyze it, we got GetTeamsEmailRequest as request (inherit from OrganizationRequest) class (mimicking those standard Request such as WhoAmIRequestUpdateRequestCreateRequest, etc). In this class, we defined the parameter that we need (DataType and Name) which is IsEmail and TeamName.

While for the response, we got GetTeamsEmailResponse as the response class (derived from OrganizationResponse) class. In this class, we just want to return an array of string attribute named Emails.

The last one, we need to create Extensions to help us cast the result (OrganizationResponse) to the custom response class that we wanted. That is why we give criteria only if T : OrganizationResponse which means only if T is derived from OrganizationResponse. I not yet found any way that we can cast the response directly to the custom response that we want currently.

Summary

You can put those Request, Response, and The extension into separate C# project / SharedProject. Then you can reuse this collection in the projects that will use it. It will optimize how you work in Plugin Development because we already have the “shortcut” of the Custom API that we made!

What do you think?

4 thoughts on “Dynamics CRM: Effective Way Calling Custom Web API In Plugin Development

  1. Great post! I believe the reference to what PluginBase is would help to make the code easier to understand.
    Andrew

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.