It’s been a while since we heard Microsoft’s desire to make Custom Actions and Workflows obsolete.
These Custom Actions can also be used for integration scenarios to add a message to the Dataverse API to be consumed at our convenience. Another good point was to create reusable logic available within components that do not need to be developed, such as Workflows and Power Automates, and thus make complex logic available to the makers/customizers.
It is only recently that Microsoft announced the notion of Custom API to both replace and extend Custom Actions.
The best way to talk about a Custom API is to consider it as a simple plug-in that will potentially take input and output parameters. Rather than developing a class implementing the CodeActivity abstract class we will simply create a plug-in that will be registered for the Main Operation stage of the Event Pipeline, so it means we do not have to register a step to trigger the logic for this new message!
When we talk about input and output parameters, many types of parameters are supported (you will notice that some of them are quite new) :
These new Custom APIs also bring several new features in addition to the new supported types, here are a few examples:
Note that there is also another parameter “IsPrivate” that cannot yet be initialized/modified using the form but only by modifying the solution files or updating the metadatas. This parameter defines whether this message will be exposed, and if not, its use by someone other than the publisher of the solution is not supported.
In this part we will therefore focus on building step by step an Action and a Function. In order to do this, we will first focus on the initialization part of the Custom API component and then the code implementation!
To create a Custom API, you can go through the dedicated forms by adding a new component from your solution (I recommend that you create a dedicated solution that will contain your Custom APIs.).
You can also do it by using web services or by creating a solution file, but you’ll understand that it’s harder 🙂
Note that a designer will be available in the future!
Once the creation form is opened, we can specify our Custom API.
I leave for the moment the field “Plugin Type” empty because we haven’t created the associated plug-in yet, we’ll update it after coding these custom APIs! For the notion of privilege, I leave that empty because I don’t want any particular restriction at this level.
Note that all read-only fields are no longer editable after creation, so you’ll have to recreate the record if you made a mistake!
We now need to add several “Custom API Request Parameter” records, linked to the Custom API created previously, to add the following parameters:
Note that for the “Name” field Microsoft recommends using a naming convention including the name of the Custom API for obvious reasons of visibility and understanding!
You’ll notice that I’ve decided to set the “Team” and “TeamName” parameters as not mandatory because I want to let the choice, but we’ll do a test within the code to make sure we have one of them!
Below is the configuration of each of them:
Now we will do the same for our CheckUserInTeam function which is quite similar as the input parameters are the same (in fact you will see that the EntityReference type is not supported for a Function, so we will have to change slightly the different parameters even if they represent the same thing in our case, it’s a known issue), but we add an output parameter and specify that this is a Function and not an Action.
Here is the definition of the Custom API:
And here is the definition of the input parameters:
As mentioned above, the purpose of a Function is that there must be a response because a GET method is used here, so we will add a Custom API Response Property!
Note that we could build this Function as an Action, the purpose here is to see both possibilities but with an Action you can also add a Response Property 🙂
In case you do not create a Response Property your “Function” Custom API will not be valid and therefore you will not be able to use it, you will get a 404 error with the following message:
{"error":{"code":"0x8006088a","message":"Resource not found for the segment 'dtv_CheckUserInTeam'."}}
If you have created a dedicated solution like me for these two actions, you should get something like this (we notice the usefulness of keeping a good name for the different names of the custom APIs and input and output parameters, especially when they have the same name 🙂 ):
The first thing to know is that the plugin must be registered on the MainOperation stage (=30) and the Mode (Synchronous/Asynchronous) must not be taken into consideration, so you will have to modify your PluginBase and make sure that this does not cause any problems with your existing plugins.
When we have finished coding the plug-in, we will need to update the “Plugin Type” field in our custom APIs to make sure the logic will be executed on the called message!
So we will create a new class “AddUserToTeam“. For my part I use a PluginBase, so I adapt accordingly by registering the new Message on the Stage MainOperation (=30) for the message “dtv_AddUserToTeam” which once triggered will execute the “Execute” function:
public AddUserToTeam() : base(typeof(AddUserToTeam))
{
RegisteredEvents.Add(
new Tuple
<MessageProcessingStepStage, string, Action<LocalContext>>(
MessageProcessingStepStage.MainOperation, MessageName.dtv_AddUserToTeam, Execute));
}
Now we can focus on the logic to be implemented, which should contain the following steps:
/// <summary>
/// Method executed when the Custom API is triggered.
/// </summary>
/// <param name="localContext"></param>
public void Execute(LocalContext localContext)
{
string teamName = localContext.PluginExecutionContext.InputParameters["TeamName"] as string;
EntityReference teamReference = localContext.PluginExecutionContext.InputParameters["Team"] as EntityReference;
if (!string.IsNullOrEmpty(teamName)
|| teamReference != null)
{
QueryExpression teamQuery = new QueryExpression()
{
EntityName = "team",
ColumnSet = new ColumnSet("teamid", "name"),
};
if (teamReference != null)
teamQuery.Criteria.AddCondition(new ConditionExpression("teamid", ConditionOperator.Equal, teamReference.Id));
else
teamQuery.Criteria.AddCondition(new ConditionExpression("name", ConditionOperator.Equal, teamName));
Entity teamEntity = localContext.OrganizationService.RetrieveMultiple(teamQuery).Entities.FirstOrDefault();
teamReference = teamEntity == null
? throw new InvalidPluginExecutionException(OperationStatus.Failed, $"No team exists with the name: {teamName}.")
: teamEntity.ToEntityReference();
AddMembersTeamRequest req = new AddMembersTeamRequest
{
TeamId = teamReference.Id,
MemberIds = new[] { localContext.PluginExecutionContext.InputParameters["User"] is EntityReference userReference
? userReference.Id
: localContext.PluginExecutionContext.InitiatingUserId }
};
localContext.OrganizationService.Execute(req);
}
else
throw new InvalidPluginExecutionException(OperationStatus.Failed, "InputParameters 'TeamName' or 'Team' is null or empty. At least one of them must be defined.");
}
Now we can start to develop the “CheckUserInTeam” Function, on the same principle as the first one, we register our event on the right message and stage:
/// <summary>
/// Initializes a new instance of the <see cref="CheckUserInTeam" /> class
/// </summary>
public CheckUserInTeam() : base(typeof(CheckUserInTeam))
{
RegisteredEvents.Add(
new Tuple
<MessageProcessingStepStage, string, Action<LocalContext>>(
MessageProcessingStepStage.MainOperation, MessageName.dtv_CheckUserInTeam, Execute));
}
Now we can focus on the logic to be implemented, which should contain the following steps:
/// <summary>
/// Method executed when the Custom API is triggered.
/// </summary>
/// <param name="localContext"></param>
public void Execute(LocalContext localContext)
{
string teamName = localContext.PluginExecutionContext.InputParameters.Contains("TeamName") ? localContext.PluginExecutionContext.InputParameters["TeamName"] as string : string.Empty;
Guid teamId = localContext.PluginExecutionContext.InputParameters.Contains("TeamGuid") ? (Guid)localContext.PluginExecutionContext.InputParameters["TeamGuid"] : Guid.Empty;
if (!string.IsNullOrEmpty(teamName)
|| teamId != Guid.Empty)
{
Guid userId = localContext.PluginExecutionContext.InputParameters.Contains("UserGuid") ? (Guid)localContext.PluginExecutionContext.InputParameters["UserGuid"] : localContext.PluginExecutionContext.InitiatingUserId;
QueryExpression teamQuery = new QueryExpression()
{
EntityName = "team",
ColumnSet = new ColumnSet("teamid", "name"),
};
if (teamId != Guid.Empty)
teamQuery.Criteria.AddCondition(new ConditionExpression("teamid", ConditionOperator.Equal, teamId));
else
teamQuery.Criteria.AddCondition(new ConditionExpression("name", ConditionOperator.Equal, teamName));
LinkEntity memberShipLink = new LinkEntity("team", "teammembership", "teamid", "teamid", JoinOperator.Inner);
LinkEntity systemUserLink = new LinkEntity("teammembership", "systemuser", "systemuserid", "systemuserid", JoinOperator.Inner);
systemUserLink.LinkCriteria.Conditions.Add(new ConditionExpression("systemuserid", ConditionOperator.Equal, userId));
memberShipLink.LinkEntities.Add(systemUserLink);
teamQuery.LinkEntities.Add(memberShipLink);
Entity matchEntity = localContext.OrganizationService.RetrieveMultiple(teamQuery).Entities.FirstOrDefault();
if (matchEntity != null)
localContext.PluginExecutionContext.OutputParameters["IsUserInTeam"] = true;
else
localContext.PluginExecutionContext.OutputParameters["IsUserInTeam"] = false;
}
else
throw new InvalidPluginExecutionException(OperationStatus.Failed, $"InputParameters 'TeamName' or 'TeamGuid' is null or empty. At least one of them must be defined.");
}
Now we have to publish our assembly and update the “Plugin Type” field of our two Custom APIs to indicate which plugin should run:
Now that we’ve been able to finalize the development part, we still have to do some tests 🙂
I’m going to use a user and create a new Team “TestAddUserToTeam“, the goal will be to test if he belongs to this team using the Function “dtv_CheckUserInTeam“, then to associate him to the team using the Action “dtv_AddUserToTeam” and finally to check that the Function returns a positive result!
You can simply use PostMan for this (you need to replace the {{WebApiUrl}} parameter with something like this “https://yourorg.crm.dynamics.com/api/data/v9.1”) :
Calling the Custom API Function “dtv_CheckUserInTeam” (you can see that the information are sent as Query Parameters and not in a body because this is an HTTP GET method):
{{WebAPIUrl}}/dtv_CheckUserInTeam(TeamName="TestAddUserToTeam",UserGuid=8ff9ca3f-f506-eb11-a812-0022480497bc)
Response from Custom API Function “dtv_CheckUserInTeam“:
{
"@odata.context": "https://yourorg.crm.dynamics.com/api/data/v9.0/$metadata#Microsoft.Dynamics.CRM.dtv_CheckUserInTeamResponse",
"IsUserInTeam": false
}
Note that if we try to call the function without at least one of the required parameters we get an error throwed by our code:
{
"error": {
"code": "0x80040265",
"message": "InputParameters 'TeamName' or 'TeamGuid' is null or empty. At least one of them must be defined."
}
}
Calling the Custom API Action “dtv_AddUserToTeam“:
{{WebAPIUrl}}/dtv_AddUserToTeam
Here is the body of the request where you can see the “User” property which is an EntityReference:
{
"TeamName": "TestAddUserToTeam",
"User": {
"systemuserid": "8ff9ca3f-f506-eb11-a812-0022480497bc",
"@odata.type": "Microsoft.Dynamics.CRM.systemuser"
}
}
Response from Custom API Action “dtv_AddUserToTeam” (You can see that there is no content because we don’t have a Custom API Response Property!):
Now, if we check the team we can see that the user is present in the team and if we execute again the Function “dtv_CheckUserInTeam” we have our output property with the value “true“:
{
"@odata.context": "https://yourorg.crm.dynamics.com/api/data/v9.0/$metadata#Microsoft.Dynamics.CRM.dtv_CheckUserInTeamResponse",
"IsUserInTeam": false
}
As a video is much better than a long speech 🙂 :
I hope you enjoyed this article, even though it is quite huge ! 🙂
Original Post https://www.blog.allandecastro.com/implementing-dataverse-custom-apis-a-k-a-new-custom-actions/