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.

Du begeisterst dich für Softwareentwicklung, hast Erfahrung mit Microsoft Technologien und suchst nach einer neuen Herausforderung? Sieh dir unsere offenen Positionen an!

(Senior) Power Platform Developer (m/w/d)

Unterstütze unser Team in der Entwicklung anspruchsvoller Lösungen - in Paderborn, Hamburg oder remote. Mehr