Introduction

Customizing Dynamics 365 Portal you can often meet an issue with the limitation of standard configuration tools provided by Microsoft. Considering that Portal is in the cloud, there is no way to access the source code (comparing to previous versions of Adxstudio). All the page settings are limited with stereotyped Liquid language. But what to do if we need, for example, to upload dynamically some data set, or run the code on the server side? Below I want to show you a small workaround, which can help to solve this type of issues.

How it works?

So let’s say, we have CRM Online and Portal installed. Our task is to create a possibility to request data from the server (which fit certain criteria, and being built dynamically) and after that we need to call some method on the server side.

First of all, we need a request acceptor, for this purpose I created a separate page on the Portal with address “/customactions”. This page will be responsible for acceptance of different commands from other Portal pages and returning the answers. This page will be only the mediator between other pages and CRM, which will allow to write a server code using Plugins and Actions (comparing with Dynamics 365 Portal, which doesn’t contain this possibility).

For CRM interaction I choose liquid tag “fetchxml”, which will send specifically configurated request.

Also we need to create a CRM Entity, which will accept the requests sent by Portal page. We can call this Entity “uds_portalactions” and put there 2 text fields with the names “uds_action” and “uds_parameters”, the functions of these fields I will describe below. After that I need to add entity entrie “Entity permission (adx_entitypermission)” where I set global rights for reading entries “uds_portalactions”.

The idea is to send the request to the page “/customactions”, which contains 4 parameters:

action – the name of the command which we need to run parameters – parameters for the command (in json format coded with the help of base64)*

attributes – same as “fetchxml” tag, it returns the set of enitites, for transforming an answer into json we need to point specific fields

cacheString – Portal like to cash the requests for fetchxml text, transferring different values we can force the portal to address the server bypassing cache

When accepting the parameters, the page create fetchxml on their basis and send them to the CRM, and when the answer is received it transfer it to json and gives away to the requesting side.

Below you can see the code of the page template:

{% assign query = request.params['action'] %}
{% assign parameters = request.params['parameters'] | replace: '"', '"' %}
{% assign attributes = request.params['attributes'] | replace: " ", "" | split: "," %}
{% assign cacheString = request.params['cacheString'] %}
{% fetchxml portalQuery %}
<fetch>
  <entity name="uds_portalactions">
    <filter type="and">
      <condition attribute="uds_action" operator="eq" value="{{query}}" />
      <condition attribute="uds_parameters" operator="eq" value="{{parameters}}" />
      <condition attribute="uds_action" operator="ne" value="{{cacheString}}" />
    </filter>
  </entity>
</fetch>
{% endfetchxml %}
{% assign result = portalQuery.results.entities %}
[
  {% for item in result %}
  {
    {% for attribute in attributes %}
    "{{attribute}}":"{{item[attribute] | string | replace: '"', '"' | replace: '', '\n'}}"
    {% if attribute != attributes.last %},
    {% endif %}
    {% endfor %}
  }
  {% if item != result.last %},
  {% endif %}
  {% endfor %}
]

1-4 is received parameters preparation

5-16 is creating request and receiving results

18-25 is transforming results into json format

Now we go to the server side. To process the requests to the Entity “uds_portalactions” we need to create 2 plugins on PRE and POST stages, which will perform all the needed actions.

First plugin (Message: RetrieveMultiple, EntityName: uds_portalaction, Stage: pre) process the requests for receiving entries:

public class QueryPlugin : IPlugin
{
    private readonly IDictionary<string, Func<string, QueryExpression>> _queryBuilderMap;

    public QueryPlugin()
    {
        _queryBuilderMap = new Dictionary<string, Func<string, QueryExpression>>()
        {
            { "TestAccountQuery", BuildTestAccountsQuery }
        };
    }

    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        FetchExpression currentQuery = context.InputParameters["Query"] as FetchExpression;

        if (currentQuery != null)
        {
            XDocument parsedQuery = XDocument.Parse(currentQuery.Query);
            Dictionary<string, string> requestParameters = parsedQuery
                .Descendants("condition")
                .Where(e =>
                    e.Attribute("attribute") != null &&
                    e.Attribute("operator") != null &&
                    e.Attribute("value") != null &&
                    String.Equals(e.Attribute("operator").Value, "eq", StringComparison.InvariantCultureIgnoreCase))
                .ToDictionary(e => e.Attribute("attribute").Value, e => e.Attribute("value").Value);

            if (requestParameters.ContainsKey("uds_action") &&
                requestParameters.ContainsKey("uds_parameters") &&
                _queryBuilderMap.ContainsKey(requestParameters["uds_action"]))
            {
                string parameters = Encoding.UTF8.GetString(Convert.FromBase64String(requestParameters["uds_parameters"]));
                QueryExpression query = _queryBuilderMap[requestParameters["uds_action"]](parameters);

                if (query != null)
                {
                    context.InputParameters["Query"] = query;
                }
            }
        }
    }

    private QueryExpression BuildTestAccountsQuery(string parameters)
    {
        TestAccountsParams typedParameters = null;
        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(TestAccountsParams));

        using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(parameters)))
        {
            typedParameters = (TestAccountsParams)serializer.ReadObject(stream);
        }

        return new QueryExpression("account")
        {
            Criteria =
            {
                Conditions =
                {
                    new ConditionExpression("name", ConditionOperator.Equal, typedParameters.Name)
                }
            }
        };
    }
}

The logic is such: before processing the request sent by the page “/customactions”, the value of it’s filters should be checked and (if needed) on the basis of received parameters we create a new request which replace the current one. So the page “/customactions” receive the set of entries from the new coded request.

The second plugin (Message: RetrieveMultiple, EntityName: uds_portalaction, Stage: post) process the requests for performing a code:

public class ActionPlugin : IPlugin
{
    private readonly IDictionary<string, Func<string, IOrganizationService, string>> _actionMap;

    public ActionPlugin()
    {
        _actionMap = new Dictionary<string, Func<string, IOrganizationService, string>>()
        {
            { "TestAccountAction", TestAccountsAction }
        };
    }

    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
        var service = serviceFactory.CreateOrganizationService(null);

        if (context.InputParameters["Query"] is FetchExpression currentQuery)
        {
            XDocument parsedQuery = XDocument.Parse(currentQuery.Query);
            Dictionary<string, string> requestParameters = parsedQuery
                .Descendants("condition")
                .Where(e =>
                    e.Attribute("attribute") != null &&
                    e.Attribute("operator") != null &&
                    e.Attribute("value") != null &&
                    String.Equals(e.Attribute("operator").Value, "eq", StringComparison.InvariantCultureIgnoreCase))
                .ToDictionary(e => e.Attribute("attribute").Value, e => e.Attribute("value").Value);

            if (requestParameters.ContainsKey("uds_action") &&
                requestParameters.ContainsKey("uds_parameters") &&
                _actionMap.ContainsKey(requestParameters["uds_action"]))
            {
                string parameters = Encoding.UTF8.GetString(Convert.FromBase64String(requestParameters["uds_parameters"]));
                string result = _actionMap[requestParameters["uds_action"]](parameters, service);

                if (context.OutputParameters["BusinessEntityCollection"] is EntityCollection resultCollection)
                {
                    Guid id = Guid.NewGuid();
                    resultCollection.Entities.Add(new Entity("uds_portalactions")
                    {
                        Id = id,
                        Attributes = { { "uds_portalactionsid", id }, { "uds_result", result } }
                    });
                }
            }
        }
    }

    private string TestAccountsAction(string parameters, IOrganizationService service)
    {
        TestAccountsParams typedParameters = null;
        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(TestAccountsParams));

        using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(parameters)))
        {
            typedParameters = (TestAccountsParams)serializer.ReadObject(stream);
        }

        OrganizationRequest testRequest = new OrganizationRequest("uds_FindAccountIdByNameAction")
        {
            Parameters = { { "AccountName", typedParameters.Name } }
        };

        OrganizationResponse response = service.Execute(testRequest);
        return response.Results["AccountId"].ToString();
    }
}

The logic is very similar with previous plugin, except we substitute not the incoming request, but it’s result with the value, which is received in the process of performing any method written in plugin. In this way we receive the possibility to request for any code allowed in CRM Plugins.

The last detail is the request from the Portal page:

var CrmQueryHelper = CrmQueryHelper || { __namespace: true };

(function(crmQueryHelperNamespace) {
  crmQueryHelperNamespace.Action = function(action, parameters, attributes, cacheString) {
    if (Object.prototype.toString.call(attributes) === '[object Array]') {
      attributes = attributes.join(',');
    }

    var request;
    var url = '/customactions?action=' + action + '&parameters=' + prepareParameters(parameters) + '&attributes=' + attributes;

    if (cacheString) {
      url += '&cacheString=' + cacheString;
    }

    var $result = $.ajax({
      type: 'GET',
      dataType: 'json',
      url: url,
      beforeSend: function(xhr) {
        request = xhr;
      }
    });

    return {
      deferred: $result,
      request: request
    };
  };

  function prepareParameters(parameters) {
    return btoa(JSON.stringify(parameters));
  }
})(CrmQueryHelper);

// Samples
CrmQueryHelper.Action('TestAccountQuery', { Name: 'Test' }, ['name', 'telephone1']).deferred.done(function(result) {
  // result
  // [{ "name": "Test", "telephone1": "4596707"}, { "name": "Test", "telephone1": "654665"} ]
});

Summary

At this moment Dynamics 365 Portal doesn’t allow any possibilities to run any code on the server side. The method described above allows partially solve this issue using standard tools of the Portal and CRM. But using these tools put us into certain limitations: the length of GET requests (in case of Portals it’s shorter than browser capabilities), potential issues with paging, ability to request the code only using XMLHttpRequest.

Note: UDS Systems provides high quality Microsoft Dynamics CRM & Dynamics 365 solutions, starting from CRM Online Customization and up to long-term On-Premise Projects with Agile methodology, industry modifications, multiple system integrations for EU, AU and US based companies.