Migrate Steps and Custom Apis to new assembly with a fully automated process
If a plugin-type needs to be renamed or removed completely in a Dataverse environment it is necessary to take some additional steps to make sure that managed solution deployments that incorporate the removal/rename of a plugin-type are still successful. As manually updating all steps and APIs to the new assembly can be tedious and error-prone we'll come up with a fully automated process.
Simply removing or renaming a plugin-type causes error
Imagine you have a plugin assembly with a plugin-type called Plugins.SamplePlugin1
and Plugins.SamplePlugin2
. If you decide to remove/rename plugin-type Plugins.SamplePlugin2
from the assembly and try to update the assembly content, you are presented with the error below:
Plug-in assembly does not contain the required types or assembly content cannot be updated.
The probably tempting approach is to just delete the plugin-type with Plugin Registration Tool. While this would work in an unmanaged scenario it will definitly fail once you move to the next stage in a managed solution and try the very same thing upfront in a pre-deployment task. Only the error message differs:
The evaluation of the current component(name=SdkMessageProcessingStep, id=XXX) in the current operation (Delete) failed during managed property evaluation of condition: {Managed Property Name: ismanaged}
Solution: Steps and custom-api need to reference the new assembly
So what is the solution? To correctly remove the plugin-type it is required to bump the assembly version on the first two digits (major.minor.build.revision), while third and fourth do not change assembly handling from a Dataverse standpoint.
After that you cannot do an in-place update but have to re-register that assembly as new and fresh assembly.
You end up with a new registered assembly without any steps associated. Furthermore, in case you have custom APIs defined, those will also reference the plugin-types in the original plugin assembly.
An automated solution for removing or renaming plugin-types
Before you now think about updating the plugin steps and custom apis by hand we will provide you with a programmatic approach to updating the steps and custom-apis to the plugin-types in the new assembly.
- Line 4: Change the assembly name
- Line 5: Change the original version number
- Line 6: Change the new version number
When you run the method MigrateStepsAndApisToNewAssemblyVersion
it will look for the assemblies in the given version number and update the associated plugin-types and apis to use the new assembly.
For diagnostic purposes new plugin-types as well as plugin-types that are not present in the new assembly are documented in lines 81/85.
static async Task Main(string[] args)
{
using var serviceClient = new ServiceClient("ADD-YOUR-CONNECTIONSTRING");
const string assemblyName = "Pragdev.Sample.Plugins";
var firstVersion = "2.0.0.0";
var secondVersion = "3.0.0.0";
await MigrateStepsAndApisToNewAssemblyVersion(serviceClient, assemblyName, firstVersion, secondVersion);
}
private static async Task MigrateStepsAndApisToNewAssemblyVersion(ServiceClient serviceClient, string assemblyName, string firstVersion, string secondVersion)
{
// Get assembly by name
var assemblies = await serviceClient.RetrieveMultipleAsync(new QueryExpression("pluginassembly")
{
ColumnSet = new ColumnSet("version"),
Criteria =
{
Conditions =
{
new ConditionExpression("name", ConditionOperator.Equal, assemblyName),
}
}
});
if (assemblies.Entities.Count == 0)
{
Console.WriteLine("Assembly {0} not found.", assemblyName);
}
var firstVersionAssembly = assemblies.Entities
.FirstOrDefault(a => string.Equals(a.GetAttributeValue<string>("version"), firstVersion));
if (firstVersionAssembly == null)
{
Console.WriteLine("Assembly {0} not found in version {1}.", assemblyName, firstVersion);
}
var secondVersionAssembly = assemblies.Entities
.FirstOrDefault(a => string.Equals(a.GetAttributeValue<string>("version"), secondVersion));
if (secondVersionAssembly == null)
{
Console.WriteLine("Assembly {0} not found in version {1}.", assemblyName, secondVersion);
}
Console.WriteLine("Searching for types in version {0}.", firstVersion);
var typesFirst = await serviceClient.RetrieveMultipleAsync(new QueryExpression("plugintype")
{
ColumnSet = new ColumnSet("typename"),
Criteria =
{
Conditions =
{
new ConditionExpression("pluginassemblyid", ConditionOperator.Equal, firstVersionAssembly.Id),
}
}
});
Console.WriteLine("Found {0} types in first assembly.", typesFirst.Entities.Count);
Console.WriteLine("Searching for types in version {0}.", secondVersion);
var typesSecond = await serviceClient.RetrieveMultipleAsync(new QueryExpression("plugintype")
{
ColumnSet = new ColumnSet("typename"),
Criteria =
{
Conditions =
{
new ConditionExpression("pluginassemblyid", ConditionOperator.Equal, secondVersionAssembly.Id),
}
}
});
Console.WriteLine("Found {0} types in second assembly.", typesSecond.Entities.Count);
var typeNamesInFirst = typesFirst.Entities.Select(e => e.GetAttributeValue<string>("typename")).OrderBy(x => x).ToList();
var typeNamesInSecond = typesSecond.Entities.Select(e => e.GetAttributeValue<string>("typename")).OrderBy(x => x).ToList();
var typesMissingInNewAssembly = typeNamesInFirst.Except(typeNamesInSecond, StringComparer.InvariantCultureIgnoreCase).ToList();
Console.WriteLine("Types missing in new Assembly: {0}", typesMissingInNewAssembly.Any() ? string.Join(", ", typesMissingInNewAssembly) : "none");
var typesNewlyCreatedInNewAssembly = typeNamesInSecond.Except(typeNamesInFirst, StringComparer.InvariantCultureIgnoreCase).ToList();
Console.WriteLine("Types newly created in new Assembly: {0}", typesNewlyCreatedInNewAssembly.Any() ? string.Join(", ", typesNewlyCreatedInNewAssembly) : "none");
foreach (var pluginType in typesSecond.Entities)
{
var typeName = pluginType.GetAttributeValue<string>("typename");
if(!typeName.Contains("preserve", StringComparison.OrdinalIgnoreCase))
{
continue;
}
Console.WriteLine("Analyzing plugintype: " + typeName);
var typeInBothAssemblies = typesFirst.Entities
.FirstOrDefault(pt =>
string.Equals(pt.GetAttributeValue<string>("typename"), typeName, StringComparison.OrdinalIgnoreCase));
if (typeInBothAssemblies != null)
{
var stepsFirst = await serviceClient.RetrieveMultipleAsync(
new QueryExpression("sdkmessageprocessingstep")
{
ColumnSet = new ColumnSet(),
Criteria =
{
Conditions =
{
new ConditionExpression("plugintypeid", ConditionOperator.Equal, typeInBothAssemblies.Id),
new ConditionExpression("category", ConditionOperator.NotEqual, "CustomAPI"),
}
}
});
Console.WriteLine("Found {0} steps for {1}.", stepsFirst.Entities.Count, typeName);
foreach (var step in stepsFirst.Entities)
{
step["plugintypeid"] = pluginType.ToEntityReference();
try
{
await serviceClient.UpdateAsync(step);
}
catch (Exception ex)
{
Console.WriteLine("Error updating step {0} for type {1}", step.Id, typeName);
Console.WriteLine(ex.ToString());
}
}
var apiFirst = await serviceClient.RetrieveMultipleAsync(
new QueryExpression("customapi")
{
ColumnSet = new ColumnSet(),
Criteria =
{
Conditions =
{
new ConditionExpression("plugintypeid", ConditionOperator.Equal, typeInBothAssemblies.Id),
}
}
});
Console.WriteLine("Found {0} apis for {1}.", apiFirst.Entities.Count, typeName);
foreach (var api in apiFirst.Entities)
{
api["plugintypeid"] = pluginType.ToEntityReference();
try
{
await serviceClient.UpdateAsync(api);
}
catch (Exception ex)
{
Console.WriteLine("Error updating api {0} for type {1}", api.Id, typeName);
Console.WriteLine(ex.ToString());
}
}
}
else
{
Console.WriteLine("Plugintype: " + typeName + " is not contained in both assemblies.");
}
}
}
Conclusion
In this article we have walked through the process of renaming/removing a plugin-type in a Dataverse environment and correctly deploying your changes through managed solutions to the target environment. Furthermore, we automated the recurring tasks to prevent errors and make sure that steps and custom-apis reference the new assembly. I hope this approach will help you next time you need to update a plugin-assembly in your managed solution.