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.

VisualStudio Dialog to change the assembly version.

After that you cannot do an in-place update but have to re-register that assembly as new and fresh assembly.

Plugin Registration Tool showing assembly in two different versions

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.


Blog post written by

Marius Pothmann

Marius Pothmann is a Microsoft certified solution architect focusing on the Power Platform and cloud-native application development on Azure. Working on the Microsoft stack for over a decade gave him the opportunity to deep dive into many building blocks of successful enterprise application architectures like Dynamics 365, SQL Server, ADFS, ASP.NET (Core), SPA-based Web Apps, WPF/WinForms. If you think he can assist in solving your challenges drop us an e-mail and we will get back to you right away.