From 9a5188204b9e569e183a98026e69b79b4af8d594 Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Mon, 25 May 2026 12:06:11 +0200 Subject: [PATCH] feat: env plugin assembly/type/step list commands --- .../Dataverse/IPluginInventoryService.cs | 76 +++++++ .../EnvironmentCliCommand.cs | 2 +- .../Assemblies/PluginAssemblyCliCommand.cs | 17 ++ .../PluginAssemblyListCliCommand.cs | 50 +++++ .../Plugin/PluginCliCommand.cs | 22 ++ .../Plugin/Steps/PluginStepCliCommand.cs | 17 ++ .../Plugin/Steps/PluginStepListCliCommand.cs | 59 ++++++ .../Plugin/Types/PluginTypeCliCommand.cs | 17 ++ .../Plugin/Types/PluginTypeListCliCommand.cs | 71 +++++++ ...eApplicationServiceCollectionExtensions.cs | 1 + .../Sdk/PluginInventoryManager.cs | 189 ++++++++++++++++++ .../DataversePluginInventoryService.cs | 29 +++ 12 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 src/TALXIS.CLI.Core/Contracts/Dataverse/IPluginInventoryService.cs create mode 100644 src/TALXIS.CLI.Features.Environment/Plugin/Assemblies/PluginAssemblyCliCommand.cs create mode 100644 src/TALXIS.CLI.Features.Environment/Plugin/Assemblies/PluginAssemblyListCliCommand.cs create mode 100644 src/TALXIS.CLI.Features.Environment/Plugin/PluginCliCommand.cs create mode 100644 src/TALXIS.CLI.Features.Environment/Plugin/Steps/PluginStepCliCommand.cs create mode 100644 src/TALXIS.CLI.Features.Environment/Plugin/Steps/PluginStepListCliCommand.cs create mode 100644 src/TALXIS.CLI.Features.Environment/Plugin/Types/PluginTypeCliCommand.cs create mode 100644 src/TALXIS.CLI.Features.Environment/Plugin/Types/PluginTypeListCliCommand.cs create mode 100644 src/TALXIS.CLI.Platform.Dataverse.Application/Sdk/PluginInventoryManager.cs create mode 100644 src/TALXIS.CLI.Platform.Dataverse.Application/Services/DataversePluginInventoryService.cs diff --git a/src/TALXIS.CLI.Core/Contracts/Dataverse/IPluginInventoryService.cs b/src/TALXIS.CLI.Core/Contracts/Dataverse/IPluginInventoryService.cs new file mode 100644 index 00000000..dadcd68c --- /dev/null +++ b/src/TALXIS.CLI.Core/Contracts/Dataverse/IPluginInventoryService.cs @@ -0,0 +1,76 @@ +namespace TALXIS.CLI.Core.Contracts.Dataverse; + +public enum PluginKind { Plugin = 0, WorkflowActivity = 1 } + +public enum PluginStage +{ + PreValidation = 10, + PreOperation = 20, + PostOperation = 40, + PostOperationDeprecated = 50, +} + +public enum PluginExecutionMode { Synchronous = 0, Asynchronous = 1 } + +public enum PluginIsolationMode { None = 1, Sandbox = 2, External = 3 } + +public enum PluginAssemblySourceType { Database = 0, Disk = 1, Gac = 2, Package = 4 } + +public sealed record PluginAssemblyRecord( + Guid Id, + string Name, + string? Version, + string? Culture, + string? PublicKeyToken, + PluginIsolationMode IsolationMode, + PluginAssemblySourceType SourceType, + string? Description, + DateTime? ModifiedOn); + +public sealed record PluginTypeRecord( + Guid Id, + string TypeName, + string? FriendlyName, + PluginKind Kind, + string? WorkflowActivityGroupName, + string? Description, + Guid AssemblyId, + string AssemblyName, + string? AssemblyVersion); + +public sealed record PluginStepRecord( + Guid Id, + string Name, + string? Description, + string Message, + string? PrimaryEntity, + PluginStage Stage, + PluginExecutionMode Mode, + int Rank, + bool Enabled, + string? FilteringAttributes, + string? Configuration, + Guid PluginTypeId, + string PluginTypeName, + Guid AssemblyId, + string AssemblyName, + string? AssemblyVersion); + +public interface IPluginInventoryService +{ + Task> ListAssembliesAsync( + string? profileName, + string? nameContains, + CancellationToken ct); + + Task> ListTypesAsync( + string? profileName, + string? assemblyContains, + PluginKind? kind, + CancellationToken ct); + + Task> ListStepsAsync( + string? profileName, + string? assemblyContains, + CancellationToken ct); +} diff --git a/src/TALXIS.CLI.Features.Environment/EnvironmentCliCommand.cs b/src/TALXIS.CLI.Features.Environment/EnvironmentCliCommand.cs index 7b3fd0ba..341fa0e3 100644 --- a/src/TALXIS.CLI.Features.Environment/EnvironmentCliCommand.cs +++ b/src/TALXIS.CLI.Features.Environment/EnvironmentCliCommand.cs @@ -6,7 +6,7 @@ namespace TALXIS.CLI.Features.Environment; Name = "environment", Alias = "env", Description = "Manage the footprint of your project in a live target environment (packages, solutions, deployment history).", - Children = new[] { typeof(Package.PackageCliCommand), typeof(Solution.SolutionCliCommand), typeof(Deployment.DeploymentCliCommand), typeof(Data.EnvDataCliCommand), typeof(Entity.EntityCliCommand), typeof(OptionSet.OptionSetCliCommand), typeof(Setting.SettingCliCommand), typeof(Changeset.ChangesetCliCommand), typeof(Component.ComponentCliCommand), typeof(Publisher.PublisherCliCommand) }, + Children = new[] { typeof(Package.PackageCliCommand), typeof(Solution.SolutionCliCommand), typeof(Deployment.DeploymentCliCommand), typeof(Data.EnvDataCliCommand), typeof(Entity.EntityCliCommand), typeof(OptionSet.OptionSetCliCommand), typeof(Setting.SettingCliCommand), typeof(Changeset.ChangesetCliCommand), typeof(Component.ComponentCliCommand), typeof(Publisher.PublisherCliCommand), typeof(Plugin.PluginCliCommand) }, ShortFormAutoGenerate = CliNameAutoGenerate.None )] public class EnvironmentCliCommand diff --git a/src/TALXIS.CLI.Features.Environment/Plugin/Assemblies/PluginAssemblyCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Plugin/Assemblies/PluginAssemblyCliCommand.cs new file mode 100644 index 00000000..a71a2dc6 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Plugin/Assemblies/PluginAssemblyCliCommand.cs @@ -0,0 +1,17 @@ +using DotMake.CommandLine; + +namespace TALXIS.CLI.Features.Environment.Plugin.Assemblies; + +[CliCommand( + Name = "assembly", + Description = "List plugin assemblies registered in the connected environment.", + Children = new[] { typeof(PluginAssemblyListCliCommand) }, + ShortFormAutoGenerate = CliNameAutoGenerate.None +)] +public class PluginAssemblyCliCommand +{ + public void Run(CliContext context) + { + context.ShowHelp(); + } +} diff --git a/src/TALXIS.CLI.Features.Environment/Plugin/Assemblies/PluginAssemblyListCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Plugin/Assemblies/PluginAssemblyListCliCommand.cs new file mode 100644 index 00000000..b41533fb --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Plugin/Assemblies/PluginAssemblyListCliCommand.cs @@ -0,0 +1,50 @@ +using DotMake.CommandLine; +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Core; +using TALXIS.CLI.Core.Contracts.Dataverse; +using TALXIS.CLI.Core.DependencyInjection; +using TALXIS.CLI.Logging; + +namespace TALXIS.CLI.Features.Environment.Plugin.Assemblies; + +[CliReadOnly] +[CliCommand( + Name = "list", + Description = "List plugin assemblies registered in the connected environment. Useful for verifying which version of an assembly is currently deployed." +)] +public class PluginAssemblyListCliCommand : ProfiledCliCommand +{ + protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(PluginAssemblyListCliCommand)); + + [CliOption(Name = "--name", Description = "Filter to assemblies whose name contains this substring.", Required = false)] + public string? Name { get; set; } + + protected override async Task ExecuteAsync() + { + var service = TxcServices.Get(); + var rows = await service.ListAssembliesAsync(Profile, Name, CancellationToken.None).ConfigureAwait(false); + + OutputFormatter.WriteList(rows, PrintTable); + return ExitSuccess; + } + +#pragma warning disable TXC003 + private static void PrintTable(IReadOnlyList rows) + { + if (rows.Count == 0) { OutputWriter.WriteLine("No plugin assemblies found."); return; } + + int nameWidth = Math.Clamp(rows.Max(r => r.Name.Length), 20, 60); + int versionWidth = Math.Clamp(rows.Max(r => (r.Version ?? "").Length), 7, 18); + string header = $"{"Name".PadRight(nameWidth)} | {"Version".PadRight(versionWidth)} | {"Isolation".PadRight(9)} | {"Source".PadRight(8)} | Modified"; + OutputWriter.WriteLine(header); + OutputWriter.WriteLine(new string('-', header.Length)); + foreach (var r in rows) + { + string name = r.Name.Length > nameWidth ? r.Name[..(nameWidth - 1)] + "." : r.Name; + string version = (r.Version ?? "").PadRight(versionWidth); + string modified = r.ModifiedOn?.ToString("yyyy-MM-dd HH:mm") ?? ""; + OutputWriter.WriteLine($"{name.PadRight(nameWidth)} | {version} | {r.IsolationMode.ToString().PadRight(9)} | {r.SourceType.ToString().PadRight(8)} | {modified}"); + } + } +#pragma warning restore TXC003 +} diff --git a/src/TALXIS.CLI.Features.Environment/Plugin/PluginCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Plugin/PluginCliCommand.cs new file mode 100644 index 00000000..02f19424 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Plugin/PluginCliCommand.cs @@ -0,0 +1,22 @@ +using DotMake.CommandLine; + +namespace TALXIS.CLI.Features.Environment.Plugin; + +[CliCommand( + Name = "plugin", + Description = "Inspect plugin assemblies, plugin types, and processing steps registered in the connected environment.", + Children = new[] + { + typeof(Assemblies.PluginAssemblyCliCommand), + typeof(Types.PluginTypeCliCommand), + typeof(Steps.PluginStepCliCommand), + }, + ShortFormAutoGenerate = CliNameAutoGenerate.None +)] +public class PluginCliCommand +{ + public void Run(CliContext context) + { + context.ShowHelp(); + } +} diff --git a/src/TALXIS.CLI.Features.Environment/Plugin/Steps/PluginStepCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Plugin/Steps/PluginStepCliCommand.cs new file mode 100644 index 00000000..6ad89f85 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Plugin/Steps/PluginStepCliCommand.cs @@ -0,0 +1,17 @@ +using DotMake.CommandLine; + +namespace TALXIS.CLI.Features.Environment.Plugin.Steps; + +[CliCommand( + Name = "step", + Description = "List plugin processing steps registered in the connected environment.", + Children = new[] { typeof(PluginStepListCliCommand) }, + ShortFormAutoGenerate = CliNameAutoGenerate.None +)] +public class PluginStepCliCommand +{ + public void Run(CliContext context) + { + context.ShowHelp(); + } +} diff --git a/src/TALXIS.CLI.Features.Environment/Plugin/Steps/PluginStepListCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Plugin/Steps/PluginStepListCliCommand.cs new file mode 100644 index 00000000..49fc4fca --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Plugin/Steps/PluginStepListCliCommand.cs @@ -0,0 +1,59 @@ +using DotMake.CommandLine; +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Core; +using TALXIS.CLI.Core.Contracts.Dataverse; +using TALXIS.CLI.Core.DependencyInjection; +using TALXIS.CLI.Logging; + +namespace TALXIS.CLI.Features.Environment.Plugin.Steps; + +[CliReadOnly] +[CliCommand( + Name = "list", + Description = "List plugin processing steps (SdkMessageProcessingStep) registered in the connected environment. Shows which message + entity each step fires on, its stage, mode, rank, and enabled state." +)] +public class PluginStepListCliCommand : ProfiledCliCommand +{ + protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(PluginStepListCliCommand)); + + [CliOption(Name = "--assembly", Description = "Filter to steps whose owning assembly name contains this substring.", Required = false)] + public string? Assembly { get; set; } + + protected override async Task ExecuteAsync() + { + var service = TxcServices.Get(); + var rows = await service.ListStepsAsync(Profile, Assembly, CancellationToken.None).ConfigureAwait(false); + + OutputFormatter.WriteList(rows, PrintTable); + return ExitSuccess; + } + +#pragma warning disable TXC003 + private static void PrintTable(IReadOnlyList rows) + { + if (rows.Count == 0) { OutputWriter.WriteLine("No plugin steps found."); return; } + + var ordered = rows + .OrderBy(r => r.AssemblyName, StringComparer.OrdinalIgnoreCase) + .ThenBy(r => r.PluginTypeName, StringComparer.OrdinalIgnoreCase) + .ThenBy(r => r.Rank) + .ToList(); + + int msgWidth = Math.Clamp(ordered.Max(r => r.Message.Length), 8, 18); + int entityWidth = Math.Clamp(ordered.Max(r => (r.PrimaryEntity ?? "").Length), 8, 22); + int typeWidth = Math.Clamp(ordered.Max(r => r.PluginTypeName.Length), 30, 60); + + string header = $"{"Message".PadRight(msgWidth)} | {"Entity".PadRight(entityWidth)} | {"Stage".PadRight(15)} | {"Mode".PadRight(5)} | {"Rank".PadRight(4)} | {"State".PadRight(8)} | {"Plugin Type".PadRight(typeWidth)} | Step Name"; + OutputWriter.WriteLine(header); + OutputWriter.WriteLine(new string('-', header.Length)); + foreach (var r in ordered) + { + string entity = (r.PrimaryEntity ?? "").PadRight(entityWidth); + string typeName = r.PluginTypeName.Length > typeWidth ? r.PluginTypeName[..(typeWidth - 1)] + "." : r.PluginTypeName; + string state = r.Enabled ? "Enabled" : "Disabled"; + string mode = r.Mode == PluginExecutionMode.Synchronous ? "Sync" : "Async"; + OutputWriter.WriteLine($"{r.Message.PadRight(msgWidth)} | {entity} | {r.Stage.ToString().PadRight(15)} | {mode.PadRight(5)} | {r.Rank.ToString().PadRight(4)} | {state.PadRight(8)} | {typeName.PadRight(typeWidth)} | {r.Name}"); + } + } +#pragma warning restore TXC003 +} diff --git a/src/TALXIS.CLI.Features.Environment/Plugin/Types/PluginTypeCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Plugin/Types/PluginTypeCliCommand.cs new file mode 100644 index 00000000..c32027ff --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Plugin/Types/PluginTypeCliCommand.cs @@ -0,0 +1,17 @@ +using DotMake.CommandLine; + +namespace TALXIS.CLI.Features.Environment.Plugin.Types; + +[CliCommand( + Name = "type", + Description = "List plugin types (plugins and workflow activities) registered in the connected environment.", + Children = new[] { typeof(PluginTypeListCliCommand) }, + ShortFormAutoGenerate = CliNameAutoGenerate.None +)] +public class PluginTypeCliCommand +{ + public void Run(CliContext context) + { + context.ShowHelp(); + } +} diff --git a/src/TALXIS.CLI.Features.Environment/Plugin/Types/PluginTypeListCliCommand.cs b/src/TALXIS.CLI.Features.Environment/Plugin/Types/PluginTypeListCliCommand.cs new file mode 100644 index 00000000..84fce5c8 --- /dev/null +++ b/src/TALXIS.CLI.Features.Environment/Plugin/Types/PluginTypeListCliCommand.cs @@ -0,0 +1,71 @@ +using DotMake.CommandLine; +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Core; +using TALXIS.CLI.Core.Contracts.Dataverse; +using TALXIS.CLI.Core.DependencyInjection; +using TALXIS.CLI.Logging; + +namespace TALXIS.CLI.Features.Environment.Plugin.Types; + +[CliReadOnly] +[CliCommand( + Name = "list", + Description = "List plugin types (plugins and workflow activities) registered in the connected environment." +)] +public class PluginTypeListCliCommand : ProfiledCliCommand +{ + protected override ILogger Logger { get; } = TxcLoggerFactory.CreateLogger(nameof(PluginTypeListCliCommand)); + + [CliOption(Name = "--assembly", Description = "Filter to plugin types whose parent assembly name contains this substring.", Required = false)] + public string? Assembly { get; set; } + + [CliOption(Name = "--kind", Description = "Filter by kind: plugin, workflow, or all (default).", Required = false)] + public string? Kind { get; set; } + + protected override async Task ExecuteAsync() + { + PluginKind? kind = null; + if (!string.IsNullOrWhiteSpace(Kind)) + { + switch (Kind.Trim().ToLowerInvariant()) + { + case "plugin": kind = PluginKind.Plugin; break; + case "workflow": + case "workflowactivity": + case "wf": kind = PluginKind.WorkflowActivity; break; + case "all": kind = null; break; + default: + Logger.LogError("Invalid --kind value '{Kind}'. Expected: plugin, workflow, or all.", Kind); + return ExitValidationError; + } + } + + var service = TxcServices.Get(); + var rows = await service.ListTypesAsync(Profile, Assembly, kind, CancellationToken.None).ConfigureAwait(false); + + OutputFormatter.WriteList(rows, PrintTable); + return ExitSuccess; + } + +#pragma warning disable TXC003 + private static void PrintTable(IReadOnlyList rows) + { + if (rows.Count == 0) { OutputWriter.WriteLine("No plugin types found."); return; } + + int typeWidth = Math.Clamp(rows.Max(r => r.TypeName.Length), 30, 70); + int assemblyWidth = Math.Clamp(rows.Max(r => r.AssemblyName.Length), 15, 45); + int versionWidth = Math.Clamp(rows.Max(r => (r.AssemblyVersion ?? "").Length), 7, 15); + string header = $"{"Type Name".PadRight(typeWidth)} | {"Kind".PadRight(16)} | {"Assembly".PadRight(assemblyWidth)} | {"Version".PadRight(versionWidth)} | WF Group"; + OutputWriter.WriteLine(header); + OutputWriter.WriteLine(new string('-', header.Length)); + foreach (var r in rows) + { + string typeName = r.TypeName.Length > typeWidth ? r.TypeName[..(typeWidth - 1)] + "." : r.TypeName; + string asm = r.AssemblyName.Length > assemblyWidth ? r.AssemblyName[..(assemblyWidth - 1)] + "." : r.AssemblyName; + string ver = (r.AssemblyVersion ?? "").PadRight(versionWidth); + string group = r.WorkflowActivityGroupName ?? ""; + OutputWriter.WriteLine($"{typeName.PadRight(typeWidth)} | {r.Kind.ToString().PadRight(16)} | {asm.PadRight(assemblyWidth)} | {ver} | {group}"); + } + } +#pragma warning restore TXC003 +} diff --git a/src/TALXIS.CLI.Platform.Dataverse.Application/DependencyInjection/DataverseApplicationServiceCollectionExtensions.cs b/src/TALXIS.CLI.Platform.Dataverse.Application/DependencyInjection/DataverseApplicationServiceCollectionExtensions.cs index 5731793c..fcb0ee7a 100644 --- a/src/TALXIS.CLI.Platform.Dataverse.Application/DependencyInjection/DataverseApplicationServiceCollectionExtensions.cs +++ b/src/TALXIS.CLI.Platform.Dataverse.Application/DependencyInjection/DataverseApplicationServiceCollectionExtensions.cs @@ -32,6 +32,7 @@ public static IServiceCollection AddTxcDataverseApplication(this IServiceCollect services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/TALXIS.CLI.Platform.Dataverse.Application/Sdk/PluginInventoryManager.cs b/src/TALXIS.CLI.Platform.Dataverse.Application/Sdk/PluginInventoryManager.cs new file mode 100644 index 00000000..ad202986 --- /dev/null +++ b/src/TALXIS.CLI.Platform.Dataverse.Application/Sdk/PluginInventoryManager.cs @@ -0,0 +1,189 @@ +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using TALXIS.CLI.Core.Contracts.Dataverse; + +namespace TALXIS.CLI.Platform.Dataverse.Application.Sdk; + +internal static class PluginInventoryManager +{ + public static async Task> ListAssembliesAsync( + IOrganizationServiceAsync2 service, + string? nameContains, + CancellationToken ct) + { + var query = new QueryExpression("pluginassembly") + { + ColumnSet = new ColumnSet( + "pluginassemblyid", "name", "version", "culture", "publickeytoken", + "isolationmode", "sourcetype", "description", "modifiedon"), + }; + if (!string.IsNullOrWhiteSpace(nameContains)) + query.Criteria.AddCondition("name", ConditionOperator.Like, $"%{nameContains}%"); + query.AddOrder("name", OrderType.Ascending); + + var rows = await RetrieveAllAsync(service, query, ct).ConfigureAwait(false); + return rows.Select(MapAssembly).ToList(); + } + + public static async Task> ListTypesAsync( + IOrganizationServiceAsync2 service, + string? assemblyContains, + PluginKind? kind, + CancellationToken ct) + { + var query = new QueryExpression("plugintype") + { + ColumnSet = new ColumnSet( + "plugintypeid", "typename", "friendlyname", "isworkflowactivity", + "workflowactivitygroupname", "description", "pluginassemblyid"), + }; + query.Criteria.AddCondition("typename", ConditionOperator.NotLike, "Compiled.Workflow%"); + + if (kind == PluginKind.Plugin) + query.Criteria.AddCondition("isworkflowactivity", ConditionOperator.Equal, false); + else if (kind == PluginKind.WorkflowActivity) + query.Criteria.AddCondition("isworkflowactivity", ConditionOperator.Equal, true); + + var assemblyLink = query.AddLink("pluginassembly", "pluginassemblyid", "pluginassemblyid", JoinOperator.Inner); + assemblyLink.EntityAlias = "a"; + assemblyLink.Columns = new ColumnSet("pluginassemblyid", "name", "version"); + if (!string.IsNullOrWhiteSpace(assemblyContains)) + assemblyLink.LinkCriteria.AddCondition("name", ConditionOperator.Like, $"%{assemblyContains}%"); + + query.AddOrder("typename", OrderType.Ascending); + + var rows = await RetrieveAllAsync(service, query, ct).ConfigureAwait(false); + return rows.Select(MapType).ToList(); + } + + public static async Task> ListStepsAsync( + IOrganizationServiceAsync2 service, + string? assemblyContains, + CancellationToken ct) + { + var query = new QueryExpression("sdkmessageprocessingstep") + { + ColumnSet = new ColumnSet( + "sdkmessageprocessingstepid", "name", "description", "mode", "stage", "rank", + "statecode", "filteringattributes", "configuration", + "plugintypeid", "sdkmessageid", "sdkmessagefilterid"), + }; + query.Criteria.AddCondition("stage", ConditionOperator.In, 10, 20, 40, 50); + + var typeLink = query.AddLink("plugintype", "plugintypeid", "plugintypeid", JoinOperator.Inner); + typeLink.EntityAlias = "pt"; + typeLink.Columns = new ColumnSet("plugintypeid", "typename", "pluginassemblyid"); + + var assemblyLink = typeLink.AddLink("pluginassembly", "pluginassemblyid", "pluginassemblyid", JoinOperator.Inner); + assemblyLink.EntityAlias = "a"; + assemblyLink.Columns = new ColumnSet("pluginassemblyid", "name", "version"); + if (!string.IsNullOrWhiteSpace(assemblyContains)) + assemblyLink.LinkCriteria.AddCondition("name", ConditionOperator.Like, $"%{assemblyContains}%"); + + var msgLink = query.AddLink("sdkmessage", "sdkmessageid", "sdkmessageid", JoinOperator.Inner); + msgLink.EntityAlias = "msg"; + msgLink.Columns = new ColumnSet("name"); + + var filterLink = query.AddLink("sdkmessagefilter", "sdkmessagefilterid", "sdkmessagefilterid", JoinOperator.LeftOuter); + filterLink.EntityAlias = "filter"; + filterLink.Columns = new ColumnSet("primaryobjecttypecode"); + + query.AddOrder("rank", OrderType.Ascending); + + var rows = await RetrieveAllAsync(service, query, ct).ConfigureAwait(false); + return rows.Select(MapStep).ToList(); + } + + private static async Task> RetrieveAllAsync( + IOrganizationServiceAsync2 service, QueryExpression query, CancellationToken ct) + { + query.PageInfo ??= new PagingInfo(); + query.PageInfo.PageNumber = 1; + query.PageInfo.PagingCookie = null; + query.PageInfo.Count = 5000; + + var all = new List(); + while (true) + { + ct.ThrowIfCancellationRequested(); + var page = await service.RetrieveMultipleAsync(query, ct).ConfigureAwait(false); + all.AddRange(page.Entities); + if (!page.MoreRecords) break; + query.PageInfo.PageNumber++; + query.PageInfo.PagingCookie = page.PagingCookie; + } + return all; + } + + private static PluginAssemblyRecord MapAssembly(Entity e) => new( + Id: e.Id, + Name: e.GetAttributeValue("name") ?? "(unknown)", + Version: e.GetAttributeValue("version"), + Culture: e.GetAttributeValue("culture"), + PublicKeyToken: e.GetAttributeValue("publickeytoken"), + IsolationMode: (PluginIsolationMode)(e.GetAttributeValue("isolationmode")?.Value ?? 1), + SourceType: (PluginAssemblySourceType)(e.GetAttributeValue("sourcetype")?.Value ?? 0), + Description: e.GetAttributeValue("description"), + ModifiedOn: e.GetAttributeValue("modifiedon")); + + private static PluginTypeRecord MapType(Entity e) + { + var assemblyRef = e.GetAttributeValue("pluginassemblyid"); + var assemblyId = assemblyRef?.Id ?? GetAliasedGuid(e, "a.pluginassemblyid"); + var isWorkflow = e.GetAttributeValue("isworkflowactivity"); + + return new PluginTypeRecord( + Id: e.Id, + TypeName: e.GetAttributeValue("typename") ?? "(unknown)", + FriendlyName: e.GetAttributeValue("friendlyname"), + Kind: isWorkflow ? PluginKind.WorkflowActivity : PluginKind.Plugin, + WorkflowActivityGroupName: e.GetAttributeValue("workflowactivitygroupname"), + Description: e.GetAttributeValue("description"), + AssemblyId: assemblyId, + AssemblyName: GetAliasedString(e, "a.name") ?? assemblyRef?.Name ?? "(unknown)", + AssemblyVersion: GetAliasedString(e, "a.version")); + } + + private static PluginStepRecord MapStep(Entity e) + { + var ptRef = e.GetAttributeValue("plugintypeid"); + var ptId = ptRef?.Id ?? GetAliasedGuid(e, "pt.plugintypeid"); + var ptName = GetAliasedString(e, "pt.typename") ?? ptRef?.Name ?? "(unknown)"; + var assemblyId = GetAliasedGuid(e, "a.pluginassemblyid"); + var assemblyName = GetAliasedString(e, "a.name") ?? "(unknown)"; + var assemblyVersion = GetAliasedString(e, "a.version"); + var message = GetAliasedString(e, "msg.name") ?? "(unknown)"; + var primaryEntity = GetAliasedString(e, "filter.primaryobjecttypecode"); + var stage = e.GetAttributeValue("stage")?.Value ?? 0; + var mode = e.GetAttributeValue("mode")?.Value ?? 0; + var statecode = e.GetAttributeValue("statecode")?.Value; + + return new PluginStepRecord( + Id: e.Id, + Name: e.GetAttributeValue("name") ?? "(unknown)", + Description: e.GetAttributeValue("description"), + Message: message, + PrimaryEntity: string.IsNullOrEmpty(primaryEntity) ? null : primaryEntity, + Stage: (PluginStage)stage, + Mode: (PluginExecutionMode)mode, + Rank: e.GetAttributeValue("rank"), + Enabled: statecode == 0, + FilteringAttributes: e.GetAttributeValue("filteringattributes"), + Configuration: e.GetAttributeValue("configuration"), + PluginTypeId: ptId, + PluginTypeName: ptName, + AssemblyId: assemblyId, + AssemblyName: assemblyName, + AssemblyVersion: assemblyVersion); + } + + private static string? GetAliasedString(Entity e, string alias) + => e.GetAttributeValue(alias)?.Value as string; + + private static Guid GetAliasedGuid(Entity e, string alias) + { + var av = e.GetAttributeValue(alias); + return av?.Value is Guid g ? g : Guid.Empty; + } +} diff --git a/src/TALXIS.CLI.Platform.Dataverse.Application/Services/DataversePluginInventoryService.cs b/src/TALXIS.CLI.Platform.Dataverse.Application/Services/DataversePluginInventoryService.cs new file mode 100644 index 00000000..5629e83b --- /dev/null +++ b/src/TALXIS.CLI.Platform.Dataverse.Application/Services/DataversePluginInventoryService.cs @@ -0,0 +1,29 @@ +using TALXIS.CLI.Core.Contracts.Dataverse; +using TALXIS.CLI.Platform.Dataverse.Application.Sdk; +using TALXIS.CLI.Platform.Dataverse.Runtime; + +namespace TALXIS.CLI.Platform.Dataverse.Application.Services; + +internal sealed class DataversePluginInventoryService : IPluginInventoryService +{ + public async Task> ListAssembliesAsync( + string? profileName, string? nameContains, CancellationToken ct) + { + using var conn = await DataverseCommandBridge.ConnectAsync(profileName, ct).ConfigureAwait(false); + return await PluginInventoryManager.ListAssembliesAsync(conn.Client, nameContains, ct).ConfigureAwait(false); + } + + public async Task> ListTypesAsync( + string? profileName, string? assemblyContains, PluginKind? kind, CancellationToken ct) + { + using var conn = await DataverseCommandBridge.ConnectAsync(profileName, ct).ConfigureAwait(false); + return await PluginInventoryManager.ListTypesAsync(conn.Client, assemblyContains, kind, ct).ConfigureAwait(false); + } + + public async Task> ListStepsAsync( + string? profileName, string? assemblyContains, CancellationToken ct) + { + using var conn = await DataverseCommandBridge.ConnectAsync(profileName, ct).ConfigureAwait(false); + return await PluginInventoryManager.ListStepsAsync(conn.Client, assemblyContains, ct).ConfigureAwait(false); + } +}