Dataverse: Create reusable C# Code/Custom API to bypass specific plugin steps

As a (lazy) Developer, I always find a way to implement something as simply as possible. The more experience that we are gaining over the years will eventually show us certain language features/components that can support it. For example, in today's blog post, we want to create a feature where we want to bypass certain plugin steps that will be called from C# code (plugin, workflow, etc) and also Power Automate. So here is the implementation!

C# Components

To achieve this demonstration, I will use a C# Shared Project to group all the extensions/classes that need to be shared across different projects:

C# Shared Project (.shproj)

Once created, in the Plugin project where you want to call the function, you can add a reference to the newly created Shared Project:

Add reference to the Shared Project

Add reference to the Shared Project

Next, I'll create an extension class inside this Shared Project, which will be the main code for this demo:

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

namespace Blog.SharedLogics.Extensions
{
    public static class OrganizationServiceExtensions
    {
        public static Guid CreateAndByPassBasedOnEnvironmentVariable(this IOrganizationService service, Entity entity, string environmentVariable)
        {
            var pluginStepIds = service.GetEnvironmentVariable(environmentVariable);

            var request = new CreateRequest
            {
                Target = entity
            };

            if (!string.IsNullOrEmpty(pluginStepIds))
            {
                request.Parameters.Add("BypassBusinessLogicExecutionStepIds", pluginStepIds);
            }

            var result = (CreateResponse)service.Execute(request);

            return result.id;
        }

        private static string GetEnvironmentVariable(this IOrganizationService service, string environmentVariable)
        {
            var query = new QueryExpression("environmentvariabledefinition")
            {
                ColumnSet = new ColumnSet("defaultvalue", "schemaname")
            };
            query.Criteria.AddCondition("schemaname", ConditionOperator.Equal, environmentVariable);

            var childLink =
                query.AddLink("environmentvariablevalue", "environmentvariabledefinitionid", "environmentvariabledefinitionid", JoinOperator.LeftOuter);
            childLink.EntityAlias = "ev";
            childLink.Columns = new ColumnSet("schemaname", "value");

            var result = service.RetrieveMultiple(query);

            var data = result.Entities.FirstOrDefault() ?? new Entity();
            var value = data.GetAttributeValue<AliasedValue>("ev.value")?.Value as string ?? data.GetAttributeValue<string>("defaultvalue");

            return value;
        }
    }
}

The logic is pretty simple. First, we need to retrieve the EnviromentVariableDefinition left join EnvironmentVariableValueand filter it by EnviromentVariableDefinition.SchemaName. Then we can get the EnvironmentVariableValue.Value. Alternatively, we can retrieve the EnvironmentVariableDefinition.DefaultValue for it (function GetEnvironmentVariable - lines 30 to 49).

Next, we just need to execute the CreateRequest, and if the value of the plugin steps that need to be bypassed is found, then we can set "BypassBusinessLogicExecutionStepIds" with that value. FYI, if the plugin steps are invalid, the Dataverse will not validate and continue to create the record.

Plugin Testing

For the demonstration via the plugin, I created below logic:

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Extensions;
using System;
using Blog.SharedLogics.Extensions;

namespace Blog.Plugins
{
    public class PreContactCreate1 : PluginBase
    {
        public PreContactCreate1()
            : base(typeof(PreContactCreate1))
        {
        }
        // Entry point for custom business logic execution
        protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext)
        {
            if (localPluginContext == null)
            {
                throw new ArgumentNullException(nameof(localPluginContext));
            }

            throw new InvalidPluginExecutionException("Error from PreContactCreate1");
        }
    }

    public class PreContactCreate2 : PluginBase
    {
        public PreContactCreate2()
            : base(typeof(PreContactCreate2))
        {
        }
        // Entry point for custom business logic execution
        protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext)
        {
            if (localPluginContext == null)
            {
                throw new ArgumentNullException(nameof(localPluginContext));
            }

            throw new InvalidPluginExecutionException("Error from PreContactCreate2");
        }
    }

    public class PreContactCreate3 : PluginBase
    {
        public PreContactCreate3()
            : base(typeof(PreContactCreate3))
        {
        }
        // Entry point for custom business logic execution
        protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext)
        {
            if (localPluginContext == null)
            {
                throw new ArgumentNullException(nameof(localPluginContext));
            }

            var target = localPluginContext.PluginExecutionContext.InputParameterOrDefault<Entity>("Target");
            target["lastname"] = "FROM PreContactCreate3";
        }
    }

    public class PostBenchmark1Create : PluginBase
    {
        public PostBenchmark1Create()
           : base(typeof(PostBenchmark1Create))
        {
        }
        // Entry point for custom business logic execution
        protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext)
        {
            if (localPluginContext == null)
            {
                throw new ArgumentNullException(nameof(localPluginContext));
            }

            var target = localPluginContext.PluginExecutionContext.InputParameterOrDefault<Entity>("Target");
            var contact = new Entity("contact")
            {
                ["firstname"] = target.GetAttributeValue<string>("tmy_name")
            };

            localPluginContext.InitiatingUserService.CreateAndByPassBasedOnEnvironmentVariable(contact, "tmy_bypasscontactcreatesteps");
        }
    }
}

The explanations will be:

  1. PreContactCreate1 will always throw an error. We will get the StepId to be passed in the Environment Variable later.
  2. PreContactCreate2 will always throw an error. We will get the StepId to be passed in the Environment Variable later.
  3. PreContactCreate3 will set the lastname of the contact to "FROM PreContactCreate3".
  4. PostBenchmark1Create will be the entry point to call the CreateAndByPassBasedOnEnvironmentVariable extensions!

After I registered the necessary plugin steps, I created the Environment Variable and set the below:

Register plugin steps and set the Environment Variable

Register plugin steps and set the Environment Variable

For the plugin step, I just need to create a Benchmark record, and here is the result that the Contact created:

Demo from the plugin (success)

Demo from the plugin (success)

And here is the failed demo:

Demo error plugin

Demo error plugin

Power Automate Testing

Because we want to call the same logic (but make it as dynamic) in the Power Automate, we need to create a Custom API, and here is the code for it:

using Blog.SharedLogics.Extensions;
using Microsoft.Xrm.Sdk.Extensions;
using Microsoft.Xrm.Sdk;
using System;

namespace Blog.Plugins.API
{
    public class CreateAndByPassBasedOnEnvironmentVariableApi : PluginBase
    {

        public const string EntityParameter = "Entity";
        public const string EntityLogicalNameParameter = "EntityLogicalName";
        public const string EnvironmentVariableParameter = "EnvironmentVariable";
        public const string EntityIdParameter = "EntityId";

        public CreateAndByPassBasedOnEnvironmentVariableApi()
           : base(typeof(CreateAndByPassBasedOnEnvironmentVariableApi))
        {
        }
        // Entry point for custom business logic execution
        protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext)
        {
            if (localPluginContext == null)
            {
                throw new ArgumentNullException(nameof(localPluginContext));
            }

            var entity = localPluginContext.PluginExecutionContext.InputParameterOrDefault<Entity>(EntityParameter);
            entity.LogicalName = localPluginContext.PluginExecutionContext.InputParameterOrDefault<string>(EntityLogicalNameParameter);
            var environmentVariable = localPluginContext.PluginExecutionContext.InputParameterOrDefault<string>(EnvironmentVariableParameter);

            var result = localPluginContext.InitiatingUserService.CreateAndByPassBasedOnEnvironmentVariable(entity, environmentVariable);
            localPluginContext.PluginExecutionContext.OutputParameters[EntityIdParameter] = result;
        }
    }
}

As you can see in the above, we are actually reusing the same component in the Dataverse Custom API (kinda making a wrapper only)!

Before we can use it in Power Automate, we need to register the Custom API:

Create Dataverse Custom API

Create Dataverse Custom API

The part where it is a little bit complex is that for this API to work, we need to use an "expando" object that has been explained by Danish here.

Last, we need to create a new Power Automate flow with these simple steps:

Parse JSON

Parse JSON

Because we need to pass the "expando" object, here is the sample JSON that I set:

{
  "@@odata.type": "#Microsoft.Dynamics.CRM.expando",
  "firstname": "Temmy PA1"
}

When you set the "Use sample payload to generate schema" and paste the value, it will generate as "@@@odata.type". You need to modify it manually to set it as "@@odata.type":

Correcting the schema

Correcting the schema

Once done, the next step is to execute the Custom API, and we need to pass the following values:

Perform the Custom API

Perform the Custom API

Here is the positive demo result:

Demo Power Automate record created result

Demo Power Automate record created result

And if I removed one of the Plugin Steps, here is the result:

Demo Power Automate record failed

Demo Power Automate record failed

Happy CRM-ing 🚀!

Leave a comment

Your comment is sent privately to the author and isn't published on the site.