From 2a446a45382b20e1c6fc450869c7ddd9630acfcd Mon Sep 17 00:00:00 2001
From: Bircck <55695195+Bircck@users.noreply.github.com>
Date: Fri, 11 Sep 2026 08:49:39 +0200
Subject: [PATCH 1/2] Fix cloud flow dependency classification
---
.../WorkflowDependencyUsageTests.cs | 28 +++++++++++++++++++
Generator/Services/AttributeMappingService.cs | 24 +---------------
Generator/Services/WorkflowService.cs | 24 ++++++++++++++--
3 files changed, 51 insertions(+), 25 deletions(-)
create mode 100644 Generator.Tests/WorkflowDependencyUsageTests.cs
diff --git a/Generator.Tests/WorkflowDependencyUsageTests.cs b/Generator.Tests/WorkflowDependencyUsageTests.cs
new file mode 100644
index 0000000..a3bc7e0
--- /dev/null
+++ b/Generator.Tests/WorkflowDependencyUsageTests.cs
@@ -0,0 +1,28 @@
+using Generator.DTO;
+using Generator.Services;
+
+namespace Generator.Tests;
+
+public class WorkflowDependencyUsageTests
+{
+ [Theory]
+ [InlineData(0, "Workflow", ComponentType.ClassicWorkflow)]
+ [InlineData(1, "Dialog", ComponentType.ClassicWorkflow)]
+ [InlineData(2, "Business Rule", ComponentType.BusinessRule)]
+ [InlineData(3, "Action", ComponentType.ClassicWorkflow)]
+ [InlineData(4, "Business Process Flow", ComponentType.ClassicWorkflow)]
+ [InlineData(5, "Power Automate Flow", ComponentType.PowerAutomateFlow)]
+ [InlineData(99, "Workflow", ComponentType.ClassicWorkflow)]
+ public void DependencyUsagePreservesWorkflowCategory(int category, string label, ComponentType componentType)
+ {
+ var workflow = new WorkflowInfo(Guid.NewGuid(), "Playground process", category, 1);
+
+ var usage = workflow.ToAttributeUsage();
+
+ Assert.Equal("Playground process", usage.Name);
+ Assert.Equal(label, usage.Usage);
+ Assert.Equal(componentType, usage.ComponentType);
+ Assert.Equal(OperationType.Other, usage.OperationType);
+ Assert.True(usage.IsFromDependencyAnalysis);
+ }
+}
diff --git a/Generator/Services/AttributeMappingService.cs b/Generator/Services/AttributeMappingService.cs
index 2c6b943..9be8625 100644
--- a/Generator/Services/AttributeMappingService.cs
+++ b/Generator/Services/AttributeMappingService.cs
@@ -61,13 +61,7 @@ public Attribute MapAttribute(
// Get workflow dependency usages
var workflowUsages = workflowDependencies
.GetValueOrDefault(metadata.MetadataId!.Value, [])
- .Select(w => new AttributeUsage(
- Name: w.Name,
- Usage: DetermineWorkflowUsageContext(w),
- OperationType: OperationType.Other,
- ComponentType: w.Category == 2 ? ComponentType.BusinessRule : ComponentType.ClassicWorkflow,
- IsFromDependencyAnalysis: true
- ))
+ .Select(w => w.ToAttributeUsage())
.ToList();
// Combine both sources
@@ -85,21 +79,5 @@ public Attribute MapAttribute(
return attr;
}
-
- ///
- /// Determines the usage context string for a workflow
- ///
- private static string DetermineWorkflowUsageContext(WorkflowInfo workflow)
- {
- return workflow.Category switch
- {
- 2 => "Business Rule",
- 0 => "Workflow",
- 3 => "Action",
- 4 => "Business Process Flow",
- 5 => "Dialog",
- _ => "Workflow"
- };
- }
}
}
diff --git a/Generator/Services/WorkflowService.cs b/Generator/Services/WorkflowService.cs
index da1ef5e..af4fcca 100644
--- a/Generator/Services/WorkflowService.cs
+++ b/Generator/Services/WorkflowService.cs
@@ -1,3 +1,4 @@
+using Generator.DTO;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
@@ -5,7 +6,7 @@
namespace Generator.Services
{
///
- /// Service responsible for querying workflow details (business rules and classic workflows)
+ /// Service responsible for querying workflow details
///
internal class WorkflowService
{
@@ -64,5 +65,24 @@ public record WorkflowInfo(
string Name,
int Category,
int Type
- );
+ )
+ {
+ public AttributeUsage ToAttributeUsage()
+ {
+ // Dataverse category 1 is Dialog; category 5 is Modern Flow.
+ // https://learn.microsoft.com/en-us/power-automate/manage-flows-with-code
+ var (usage, componentType) = Category switch
+ {
+ 1 => ("Dialog", ComponentType.ClassicWorkflow),
+ 2 => ("Business Rule", ComponentType.BusinessRule),
+ 3 => ("Action", ComponentType.ClassicWorkflow),
+ 4 => ("Business Process Flow", ComponentType.ClassicWorkflow),
+ 5 => ("Power Automate Flow", ComponentType.PowerAutomateFlow),
+ _ => ("Workflow", ComponentType.ClassicWorkflow)
+ };
+
+ return new AttributeUsage(Name, usage, OperationType.Other, componentType,
+ IsFromDependencyAnalysis: true);
+ }
+ }
}
From 912cb50040f25d185ce2bc1a9952b5b67da15d2e Mon Sep 17 00:00:00 2001
From: Bircck <55695195+Bircck@users.noreply.github.com>
Date: Fri, 11 Sep 2026 10:01:30 +0200
Subject: [PATCH 2/2] Include current cloud-flow actions and whole-table
workflow dependencies
---
.../ActionAnalyzersTests.cs | 1 +
.../PowerAutomateFlowAnalyzerTests.cs | 36 +++++++++++++++++++
Generator/DataverseService.cs | 12 ++-----
.../Analyzers/UpdateRowAnalyzer.cs | 2 +-
.../Services/SolutionComponentService.cs | 20 +++++------
5 files changed, 50 insertions(+), 21 deletions(-)
diff --git a/Generator.Tests/PowerAutomateAnalyzerTests/ActionAnalyzersTests.cs b/Generator.Tests/PowerAutomateAnalyzerTests/ActionAnalyzersTests.cs
index bf2cbfe..8a7cb14 100644
--- a/Generator.Tests/PowerAutomateAnalyzerTests/ActionAnalyzersTests.cs
+++ b/Generator.Tests/PowerAutomateAnalyzerTests/ActionAnalyzersTests.cs
@@ -231,6 +231,7 @@ public void UpdateRowAnalyzer_SupportedOperationIds_ShouldContainExpectedValues(
// Assert
Assert.Contains("UpdateRecord", operationIds);
+ Assert.Contains("UpdateOnlyRecord", operationIds);
Assert.Contains("PatchItem", operationIds);
}
diff --git a/Generator.Tests/PowerAutomateAnalyzerTests/PowerAutomateFlowAnalyzerTests.cs b/Generator.Tests/PowerAutomateAnalyzerTests/PowerAutomateFlowAnalyzerTests.cs
index 820a2ce..501c802 100644
--- a/Generator.Tests/PowerAutomateAnalyzerTests/PowerAutomateFlowAnalyzerTests.cs
+++ b/Generator.Tests/PowerAutomateAnalyzerTests/PowerAutomateFlowAnalyzerTests.cs
@@ -140,6 +140,42 @@ public async Task AnalyzeComponentAsync_WithCreateRowAction_ShouldNotThrow()
Assert.NotNull(attributeUsages);
}
+ [Fact]
+ public async Task AnalyzeComponentAsync_WithUpdateOnlyRecord_ShouldExtractCloudFlowUpdateUsage()
+ {
+ var action = JObject.Parse(@"{
+ 'type': 'OpenApiConnection',
+ 'inputs': {
+ 'host': {
+ 'apiId': '/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps',
+ 'operationId': 'UpdateOnlyRecord'
+ },
+ 'parameters': {
+ 'entityName': 'dmvp_planets',
+ 'recordId': '00000000-0000-0000-0000-000000000001',
+ 'item/dmvp_weight': 42
+ }
+ }
+ }");
+ var flowJson = new PowerAutomateFlowBuilder()
+ .AddAction("Update_planet", action)
+ .BuildAsJson();
+ var flow = new PowerAutomateFlow("synthetic-flow", "Playground weight update", flowJson);
+ var attributeUsages = new Dictionary>>();
+ var warnings = new List();
+
+ await FlowAnalyzer.AnalyzeComponentAsync(flow, attributeUsages, warnings);
+
+ Assert.True(attributeUsages.ContainsKey("dmvp_planets"));
+ Assert.True(attributeUsages["dmvp_planets"].ContainsKey("dmvp_weight"));
+ var usage = Assert.Single(attributeUsages["dmvp_planets"]["dmvp_weight"]);
+ Assert.Equal("Playground weight update", usage.Name);
+ Assert.Equal(OperationType.Update, usage.OperationType);
+ Assert.Equal(ComponentType.PowerAutomateFlow, usage.ComponentType);
+ Assert.False(usage.IsFromDependencyAnalysis);
+ Assert.Contains("Update parameter", usage.Usage);
+ Assert.Empty(warnings);
+ }
[Fact]
public async Task AnalyzeComponentAsync_WithUpdateRowAction_ShouldNotThrow()
{
diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs
index 555b294..e93ab5a 100644
--- a/Generator/DataverseService.cs
+++ b/Generator/DataverseService.cs
@@ -218,16 +218,8 @@ public DataverseService(
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Getting workflow dependencies for attributes");
// Get workflow dependencies for attributes (returns attribute ObjectId -> list of workflow ObjectIds)
- var explicitComponentsList = solutionComponents.ToList();
- var workflowDependencyMap = await solutionComponentService.GetWorkflowDependenciesForAttributesAsync(
- explicitComponentsList.Where(c => c.ComponentType == 2).Select(c => new SolutionComponentInfo(
- c.ObjectId,
- c.SolutionComponentId ?? Guid.Empty,
- c.ComponentType,
- c.RootComponentBehaviour,
- new EntityReference("solution", c.SolutionId)
- ))
- );
+ // Scan the same attributes we export, including columns included with a whole table.
+ var workflowDependencyMap = await solutionComponentService.GetWorkflowDependenciesForAttributesAsync(attributesInSolution);
// Get workflow details for all unique workflow IDs
var allWorkflowIds = workflowDependencyMap.Values.SelectMany(ids => ids).Distinct().ToList();
diff --git a/Generator/Services/Power Automate/Analyzers/UpdateRowAnalyzer.cs b/Generator/Services/Power Automate/Analyzers/UpdateRowAnalyzer.cs
index 8352980..a2621c8 100644
--- a/Generator/Services/Power Automate/Analyzers/UpdateRowAnalyzer.cs
+++ b/Generator/Services/Power Automate/Analyzers/UpdateRowAnalyzer.cs
@@ -11,7 +11,7 @@ public class UpdateRowAnalyzer : DataverseActionAnalyzerBase
{
public override IEnumerable SupportedOperationIds => new[]
{
- "UpdateRow", "UpdateRecord", "UpdateItem", "PatchItem"
+ "UpdateRow", "UpdateRecord", "UpdateOnlyRecord", "UpdateItem", "PatchItem"
};
public override ActionAnalysisResult Analyze(JToken action, string actionName)
diff --git a/Generator/Services/SolutionComponentService.cs b/Generator/Services/SolutionComponentService.cs
index 895c52a..2a0bf78 100644
--- a/Generator/Services/SolutionComponentService.cs
+++ b/Generator/Services/SolutionComponentService.cs
@@ -217,10 +217,10 @@ private async Task> GetComponentNodesAsync(List no
return results;
}
- private HashSet GetDependentComponents(IEnumerable components)
+ private HashSet GetAttributeDependentComponents(IEnumerable attributeIds)
{
var results = new HashSet();
- var componentsList = components.ToList();
+ var componentsList = attributeIds.Distinct().ToList();
int totalCount = componentsList.Count;
int processedCount = 0;
int errorCount = 0;
@@ -250,8 +250,8 @@ private HashSet GetDependentComponents(IEnumerable GetDependentComponents(IEnumerable GetRequiredComponents(IEnumerable
/// Gets workflow dependencies for attributes by finding workflows (type 29) that depend on specified attributes
///
- /// List of attribute components to check for dependencies
+ /// Metadata IDs of exported attributes, including whole-table subcomponents
/// Dictionary mapping attribute ObjectId to list of workflow ObjectIds that depend on it
- public async Task>> GetWorkflowDependenciesForAttributesAsync(IEnumerable attributeComponents)
+ public async Task>> GetWorkflowDependenciesForAttributesAsync(IEnumerable attributeIds)
{
var workflowDependencies = new Dictionary>();
- // Filter to only attributes (component type 2)
- var attributes = attributeComponents.Where(c => c.ComponentType == 2).ToList();
+ // Whole-table additions may have no individual solutioncomponent rows.
+ var attributes = attributeIds.Distinct().ToList();
if (!attributes.Any())
{
@@ -390,7 +390,7 @@ public async Task>> GetWorkflowDependenciesForAttrib
_logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Checking {attributes.Count} attributes for workflow dependencies");
// Get all dependent components for attributes
- var dependencies = GetDependentComponents(attributes);
+ var dependencies = GetAttributeDependentComponents(attributes);
_logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Found {dependencies.Count} total dependencies for attributes");
if (!dependencies.Any())