Skip to content

Plugin Analysis

Lucki2g edited this page Jan 31, 2026 · 4 revisions

Plugin Analysis System Documentation

This document provides comprehensive documentation for the Generator's static code analysis system for Dataverse plugins. This system performs Roslyn-based static analysis of C# source code to detect which entity attributes are used by plugins, without requiring runtime execution.

Note

This analysis is separate from PluginAnalyzer.cs, which uses the SDK Step registrations from Dataverse metadata. This documentation covers the code-based static analysis approach enabled via feature features: static code analysis.

Caution

The analysis expects you to use XrmContext for early-bound .NET types and same pluginregistration method as XrmFramework or XrmBedrock.


Table of Contents

  1. Architecture Overview
  2. Component Descriptions
  3. Data Flow
  4. Detected Patterns
  5. Limitations
  6. Code Examples

Architecture Overview

The plugin analysis system consists of 5 core components that work together:

┌─────────────────────────────────────────────────────────────────────────────┐
│                           DataverseService.cs                                │
│                      (Orchestrator / Entry Point)                            │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                    ┌───────────────┴───────────────┐
                    │                               │
                    ▼                               ▼
┌───────────────────────────────────┐   ┌───────────────────────────────────┐
│      EntityMetadataParser         │   │      EntityMetadataParser         │
│   (Parses XrmContext.cs for       │   │   (Shared metadata source)        │
│    entity/attribute mappings)     │   │                                   │
└───────────────────────────────────┘   └───────────────────────────────────┘
                    │                               │
                    ▼                               ▼
┌───────────────────────────────────┐   ┌───────────────────────────────────┐
│   PluginRegistrationAnalyzer      │   │      BusinessLogicAnalyzer        │
│   (Analyzes Plugin classes for    │   │   (Analyzes Manager/Service       │
│    step registrations & logic)    │   │    classes for attribute access)  │
└───────────────────────────────────┘   └───────────────────────────────────┘
                    │                               │
                    └───────────────┬───────────────┘
                                    ▼
                    ┌───────────────────────────────────┐
                    │       AttributeAccessVisitor      │
                    │   (Roslyn syntax walker for       │
                    │    detecting attribute accesses)  │
                    └───────────────────────────────────┘

Key Design Principles

  1. Static Analysis Only: No runtime execution; purely syntax-based detection
  2. Cached Parsing: XrmContext.cs parsing is cached to avoid repeated expensive operations
  3. Strong Typing: Uses XrmContext.cs to map C# property names → Dataverse logical names
  4. Pattern Recognition: Detects multiple patterns for entity/attribute access

Component Descriptions

1. EntityMetadataParser

Purpose: Parses the auto-generated XrmContext.cs file to build a mapping between:

  • C# class names → Entity logical names (e.g., Accountaccount)
  • C# property names → Attribute logical names (e.g., Namename, AccountNumberaccountnumber)
  • Relationship navigation properties → Schema names

Key Features:

  • SHA256-based caching: Computes file hash and caches results to .xrmcontext-metadata.cache
  • Extracts [EntityLogicalName] and [AttributeLogicalName] decorators
  • Extracts [RelationshipSchemaName] for navigation properties

Key Methods:

// Parse XrmContext.cs (uses cache if unchanged)
await parser.ParseAsync("path/to/XrmContext.cs");

// Get entity by C# class name
EntityInfo? entity = parser.GetEntityByClassName("Account");

// Get entity by Dataverse logical name
EntityInfo? entity = parser.GetEntityByLogicalName("account");

// Map property to logical name
string? logicalName = parser.GetAttributeLogicalName("Account", "Name"); // Returns "name"

Data Structures:

public record EntityInfo(
    string ClassName,      // "Account"
    string LogicalName,    // "account"
    int? EntityTypeCode    // 1
)
{
    public Dictionary<string, AttributeInfo> Attributes { get; set; }
    public Dictionary<string, RelationshipInfo> Relationships { get; set; }
}

public record AttributeInfo(
    string PropertyName,   // "Name"
    string LogicalName,    // "name"
    string? Type,          // "string"
    string? DisplayName    // "Account Name"
);

public record RelationshipInfo(
    string PropertyName,          // "Contacts"
    string SchemaName,            // "account_contacts"
    string? RelatedEntityClassName, // "Contact"
    string? EntityRole,           // "Referenced" or "Referencing"
    bool IsCollection             // true for 1:N
);

2. PluginRegistrationAnalyzer

Purpose: Analyzes plugin class files to extract:

  1. Step Registrations: From RegisterPluginStep<T>() (note that is from Bedrock's or Framework's Plugin.cs extending interface IPlugin)
  2. Filtered Attributes: From .AddFilteredAttributes([...])
  3. Image Attributes: From .AddImage(ImageType.PreImage, [...])
  4. Execute Logic: Attributes accessed in the handler method/lambda

Key Method:

var analyzer = new PluginRegistrationAnalyzer(metadataParser, verbose: true);
List<PluginStepInfo> steps = await analyzer.AnalyzeDirectoryAsync("path/to/Plugins");

Detected Patterns:

a) Step Registration Detection

// Pattern 1: Direct call
RegisterPluginStep<Account>(EventOperation.Update, ExecutionStage.PreOperation, Execute);

// Pattern 2: Chained with 'this'
this.RegisterPluginStep<Account>(EventOperation.Create, ExecutionStage.PostOperation, ctx => { });

b) Filtered Attributes Detection

// Collection expression syntax (C# 12)
.AddFilteredAttributes([acc => acc.Name, acc => acc.AccountNumber])

// Individual lambdas
.AddFilteredAttributes(acc => acc.Name, acc => acc.AccountNumber)

c) Image Attributes Detection

.AddImage(ImageType.PreImage, [acc => acc.Name, acc => acc.Revenue])
.AddImage(ImageType.PostImage, acc => acc.OwnerId)

d) Execute Method Analysis

The analyzer traces:

  1. GetTargetEntity<T>(), GetPreImage<T>(), GetPostImage<T>() calls
  2. Variable assignments from these calls
  3. All member accesses on those variables
public void Execute(IPluginContext context)
{
    var target = context.GetTargetEntity<Account>(); // Tracked: target -> Account
    var preImage = context.GetPreImage<Account>();   // Tracked: preImage -> Account
    
    target.Name = "New Name";        // Detected: account.name (Write)
    var revenue = preImage.Revenue;  // Detected: account.revenue (Read)
}

Output Data Structure:

public record PluginStepInfo(
    string ClassName,           // "UpdateAccountPlugin"
    string FullName,            // "MyPlugins.Accounts.UpdateAccountPlugin"
    string FilePath,            // "C:/Plugins/UpdateAccountPlugin.cs"
    string EntityLogicalName,   // "account"
    string EntityClassName,     // "Account"
    string EventOperation,      // "Update"
    string ExecutionStage,      // "PreOperation"
    string ExecutionMode,       // "Synchronous" or "Asynchronous"
    string? UsesManager,        // "AccountManager" (if CreatePluginManager<T> detected)
    string? UsesService         // "AccountService" (if GetRequiredService<T> detected)
)
{
    public List<string> FilteredAttributes { get; set; }      // ["name", "accountnumber"]
    public List<ImageInfo> Images { get; set; }               // Image configurations
    public List<string> UsedAttributesInLogic { get; set; }   // ["name", "revenue"]
}

3. BusinessLogicAnalyzer

Purpose: Analyzes Manager and Service classes (business logic layer) for entity attribute access patterns. Uses AttributeAccessVisitor internally.

Key Method:

var analyzer = new BusinessLogicAnalyzer(metadataParser);
int fileCount = await analyzer.AnalyzeDirectoryAsync("path/to/BusinessLogic", attributeUsages);

Behavior:

  • Recursively scans all .cs files (excluding bin/ and obj/)
  • For each class, invokes AttributeAccessVisitor to detect attribute accesses
  • Results are added directly to the shared attributeUsages dictionary

4. AttributeAccessVisitor

Purpose: A Roslyn CSharpSyntaxWalker that walks the entire syntax tree to detect all patterns of entity attribute access.

Detected Access Patterns (enumerated in AccessPatternType):

Pattern Description Example
PropertyRead Direct property read account.Name
PropertyWrite Direct property assignment account.Name = "Test"
LinqWhere LINQ filter condition .Where(a => a.Status == 1)
LinqSelect LINQ projection .Select(a => a.Name)
GetAttributeValue Dynamic attribute read entity.GetAttributeValue<string>("name")
SetAttributeValue Dynamic attribute write entity.SetAttributeValue("name", "Test")
AnonymousObject Anonymous type projection new { a.Id, a.Name }
ObjectInitializerWrite Object initializer new Account { Name = "Test" }
ObjectInitializerRead Initializer right-side read new Contact { ParentAccount = acc.Id }

Operation Type Detection:

The visitor attempts to determine if an access is for Create, Read, Update, Delete, or List based on:

  1. Method name heuristics: Method containing "Create", "Update", "Delete", etc.
  2. Object construction: new Entity() without ID = Create, with ID = Update
  3. LINQ context: Where, Any = List; FirstOrDefault = Read; Select = Read/projection

Lambda Parameter Tracking:

The visitor maintains a dictionary of lambda parameters to their entity types:

// When visiting: context.AccountSet.Where(a => a.Name == "Test")
// The visitor tracks: _lambdaParameterTypes["a"] = "Account"
// So when visiting a.Name, it knows "a" is an Account entity

5. SyntaxHelpers

Purpose: Shared utility methods for Roslyn syntax analysis.

Key Method:

// Gets fully qualified type name including namespace
string fullName = SyntaxHelpers.GetFullTypeName(classDecl);
// Returns: "MyProject.Plugins.Accounts.UpdateAccountPlugin"

Data Flow

1. Configuration
   ├── CodeAnalysis:XrmContextPath = "../XrmContext.cs"
   ├── CodeAnalysis:PluginProjectPaths = "../Plugins;../CustomAPIs"
   └── CodeAnalysis:LogicPaths = "../BusinessLogic"

2. XrmContext Parsing
   └── EntityMetadataParser.ParseAsync()
       ├── Computes SHA256 hash
       ├── Checks cache (.xrmcontext-metadata.cache)
       ├── If cache miss: Parse with Roslyn
       └── Returns: Dictionary<ClassName, EntityInfo>

3. Plugin Analysis (per directory)
   └── PluginRegistrationAnalyzer.AnalyzeDirectoryAsync()
       ├── Find all .cs files (exclude bin/obj)
       ├── For each file:
       │   ├── Parse with Roslyn
       │   ├── Find classes inheriting Plugin/CustomAPI
       │   ├── Find RegisterPluginStep<T>() calls
       │   ├── Parse fluent builder chain
       │   └── Analyze Execute method
       └── Returns: List<PluginStepInfo>

4. Business Logic Analysis (per directory)
   └── BusinessLogicAnalyzer.AnalyzeDirectoryAsync()
       ├── Find all .cs files (exclude bin/obj)
       └── For each file:
           └── AttributeAccessVisitor.Analyze()
               ├── Walk entire syntax tree
               ├── Track lambda parameter types
               ├── Record all attribute accesses
               └── Add to attributeUsages dictionary

5. Output Aggregation
   └── attributeUsages: Dictionary<EntityLogicalName, Dictionary<AttributeLogicalName, List<AttributeUsage>>>
       Example:
       {
         "account": {
           "name": [
             AttributeUsage("UpdateAccountPlugin", "Filtered (Update)", Update, Plugin, true),
             AttributeUsage("AccountManager", "AccountManager: LINQ Where", List, Plugin, true)
           ],
           "revenue": [
             AttributeUsage("UpdateAccountPlugin", "PreImage Image", Read, Plugin, true)
           ]
         }
       }

Limitations

1. No Semantic Analysis ⚠️ Critical

The analyzer uses syntax-only analysis (no Roslyn compilation). This means:

// ❌ CANNOT DETECT - Type inference through var
var entity = GetSomeEntity();  // Type unknown!
entity.Name = "Test";          // Won't be detected

// ✅ DETECTED - Explicit type or tracked patterns
Account entity = GetSomeEntity();
var entity = context.GetTargetEntity<Account>();  // Tracked

Workaround: Use explicit generic methods like GetTargetEntity<Account>() instead of untyped patterns.


2. Limited Variable Tracking

Only variables assigned from specific patterns are tracked:

// ✅ TRACKED
var target = context.GetTargetEntity<Account>();
var preImage = context.GetPreImage<Account>();
Account acc = (Account)entity;
var acc = entity as Account;

// ❌ NOT TRACKED
var target = GetTarget();              // Non-generic method
var acc = accountService.GetAccount(); // Service method
var acc = _accounts.First();           // LINQ result assignment

3. No Cross-File Analysis

Each file is analyzed in isolation:

// ❌ CANNOT DETECT - Method in different file
// File: AccountManager.cs
public void UpdateName(Account acc, string name) {
    acc.Name = name;  // Won't know 'acc' is Account unless tracked in THIS file
}

// File: Plugin.cs
var manager = new AccountManager();
manager.UpdateName(target, "Test");  // Call not followed

4. Interface/Abstract Type Blindness

// ❌ CANNOT DETECT
IEntity entity = target;
entity.Name = "Test";  // IEntity is not a known entity class

// ❌ CANNOT DETECT - Generic constraints
public void Process<T>(T entity) where T : Entity {
    entity.Name = "Test";  // T is not resolved to concrete type
}

5. Dynamic Attribute Access Limitations

// ✅ DETECTED - String literal
entity.GetAttributeValue<string>("name");

// ❌ NOT DETECTED - Variable/constant
const string NameField = "name";
entity.GetAttributeValue<string>(NameField);

// ❌ NOT DETECTED - Interpolation/concatenation
entity.GetAttributeValue<string>($"prefix_{fieldName}");

6. Reflection/Dynamic Not Supported

// ❌ NOT DETECTED
entity["name"] = "Test";                    // Indexer access
entity.Attributes["name"] = "Test";         // Dictionary access
typeof(Account).GetProperty("Name");        // Reflection
dynamic dyn = entity; dyn.Name = "Test";    // Dynamic

7. Conditional/Complex Logic Not Tracked

The analyzer cannot determine if an attribute is conditionally accessed:

// ⚠️ DETECTED AS USED (even though conditionally)
if (someCondition)
{
    target.Name = "Test";  // Detected, but condition not tracked
}

// ⚠️ BOTH DETECTED (no branch analysis)
target.Name = flag ? "A" : "B";  // Simple case, detected
target.Name = GetValue(target.Revenue);  // Both Name and Revenue detected

8. Only Supports Specific Plugin Registration Pattern

Only the fluent RegisterPluginStep<T> pattern is detected:

// ✅ SUPPORTED
RegisterPluginStep<Account>(EventOperation.Update, ExecutionStage.PreOperation, Execute)
    .AddFilteredAttributes([a => a.Name])
    .AddImage(ImageType.PreImage, [a => a.Revenue]);

// ❌ NOT SUPPORTED - Attribute-based registration
[CrmPluginRegistration("Update", "account", ...)]
public class MyPlugin : IPlugin { }

// ❌ NOT SUPPORTED - Manual registration
var step = new SdkMessageProcessingStep { ... };

9. Partial Type Resolution

If XrmContext.cs doesn't contain an entity, it won't be tracked:

// ❌ NOT DETECTED - Entity not in XrmContext.cs
var custom = context.GetTargetEntity<my_customentity>();  // Class not generated
custom.my_field = "Test";

10. No Inheritance Tracking

// Base class
public class BaseAccountPlugin : Plugin {
    protected void UpdateName(Account acc) {
        acc.Name = "Test";  // Detected in base class analysis
    }
}

// Derived class
public class SpecificPlugin : BaseAccountPlugin {
    public void Execute() {
        UpdateName(target);  // Base class method not re-analyzed
    }
}

Code Examples

Example 1: Fully Detected Plugin

// ✅ All accesses will be detected
public class UpdateAccountPlugin : Plugin
{
    public UpdateAccountPlugin()
    {
        RegisterPluginStep<Account>(
            EventOperation.Update,
            ExecutionStage.PreOperation,
            Execute)
            .AddFilteredAttributes([a => a.Name, a => a.AccountNumber])  // ✅ Filtered
            .AddImage(ImageType.PreImage, [a => a.Revenue, a => a.OwnerId]);  // ✅ Image
    }

    private void Execute(IPluginContext context)
    {
        var target = context.GetTargetEntity<Account>();   // ✅ Variable tracked
        var preImage = context.GetPreImage<Account>();     // ✅ Variable tracked
        
        // ✅ All these are detected
        if (target.Name != preImage.Name)                  // Read on target.Name, preImage.Name
        {
            target.Description = "Name changed";           // Write on target.Description
        }
        
        var oldRevenue = preImage.Revenue;                 // Read on preImage.Revenue
    }
}

Detected Usages:

Attribute Usage Operation
name Filtered (Update) Update
accountnumber Filtered (Update) Update
revenue PreImage Image Read
ownerid PreImage Image Read
name Used in Logic (Update) Update
description Used in Logic (Update) Update
revenue Used in Logic (Update) Read

Example 2: Partially Detected Plugin

public class ProblematicPlugin : Plugin
{
    public ProblematicPlugin()
    {
        RegisterPluginStep<Account>(
            EventOperation.Update,
            ExecutionStage.PreOperation,
            Execute);
    }

    private void Execute(IPluginContext context)
    {
        // ❌ NOT TRACKED - var with non-generic method
        var target = context.GetTarget();
        target.Name = "Test";  // ❌ Won't be detected (target type unknown)
        
        // ❌ NOT TRACKED - Service method
        var account = _accountService.GetById(id);
        account.Revenue = 100;  // ❌ Won't be detected
        
        // ❌ NOT TRACKED - Variable attribute name
        string field = "name";
        target.SetAttributeValue(field, "Test");  // ❌ Won't be detected
        
        // ✅ TRACKED - Direct generic call  
        var proper = context.GetTargetEntity<Account>();
        proper.Description = "Test";  // ✅ Detected
    }
}

Example 3: Business Logic Analysis

// File: AccountManager.cs
public class AccountManager
{
    private readonly XrmContext _context;
    
    public List<Account> GetActiveAccounts()
    {
        // ✅ DETECTED - LINQ Where
        return _context.AccountSet
            .Where(a => a.StateCode == 0)           // ✅ account.statecode (List)
            .Where(a => a.Name.Contains("Corp"))    // ✅ account.name (List)
            .OrderBy(a => a.CreatedOn)              // ✅ account.createdon (List)
            .Select(a => new Account {              // Object initializer
                Id = a.Id,                          // ✅ account.accountid (Read)
                Name = a.Name,                      // ✅ account.name (Read)
                Revenue = a.Revenue                 // ✅ account.revenue (Read)
            })
            .ToList();
    }
    
    public void UpdateRevenue(Account account, decimal amount)
    {
        account.Revenue = amount;                   // ✅ account.revenue (Update)
        account.ModifiedOn = DateTime.Now;          // ✅ account.modifiedon (Update)
    }
}

Example 4: Relationship Navigation

public class ContactPlugin : Plugin
{
    private void Execute(IPluginContext context)
    {
        var contact = context.GetTargetEntity<Contact>();
        
        // ✅ DETECTED - Navigation property access  
        var parentAccount = contact.ParentCustomerId;      // contact.parentcustomerid
        
        // ⚠️ PARTIALLY DETECTED - Depends on relationship mapping
        // If XrmContext has RelationshipSchemaName attribute:
        var primaryContact = contact.Account.PrimaryContactId;
        // contact → Account (via relationship) → primarycontactid
    }
}

Best Practices for Maximum Detection

  1. Use Strongly-Typed Generic Methods

    // ✅ Good
    var target = context.GetTargetEntity<Account>();
    
    // ❌ Avoid
    var target = context.GetTarget();
  2. Use String Literals for Dynamic Access

    // ✅ Detectable
    entity.GetAttributeValue<string>("name");
    
    // ❌ Not detectable
    entity.GetAttributeValue<string>(FieldConstants.Name);
  3. Register Filtered Attributes Explicitly

    // ✅ Fully tracked
    .AddFilteredAttributes([a => a.Name, a => a.Revenue])
  4. Use Standard Variable Names for Fallback

    // ✅ Fallback detection for common names
    var target = ...;      // Assumed to be registered entity type
    var preImage = ...;    // Assumed to be registered entity type
    var entity = ...;      // Assumed to be registered entity type
  5. Keep Plugin Logic in the Plugin Class

    // ✅ Fully analyzed
    public class MyPlugin : Plugin {
        private void Execute(IPluginContext ctx) {
            // All logic here
        }
    }
    
    // ⚠️ Separate analysis (may miss connections)
    public class MyPlugin : Plugin { ... }
    public class MyManager { ... }  // Analyzed separately

Configuration

In appsettings.json:

{
  "CodeAnalysis": {
    "XrmContextPath": "/../XrmContext/XrmContext.cs",
    "PluginProjectPaths": "/../Plugins;/../CustomAPIs",
    "LogicPaths": "/../BusinessLogic/Managers;/../BusinessLogic/Services"
  }
}
  • XrmContextPath: Path to the XrmContext.cs file (relative to Generator directory)
  • PluginProjectPaths: Semicolon-separated paths to plugin projects
  • LogicPaths: Semicolon-separated paths to business logic directories

Summary

Component Purpose Key Limitation
EntityMetadataParser Maps C# names → logical names Only parses XrmContext.cs
PluginRegistrationAnalyzer Extracts step registrations Only RegisterPluginStep<T> pattern
BusinessLogicAnalyzer Analyzes Manager/Service classes Per-file isolation
AttributeAccessVisitor Detects attribute access patterns Syntax-only (no type inference)
SyntaxHelpers Utility methods -

Overall Accuracy: ~70-85% for well-structured codebases following best practices. Lower accuracy for dynamic, reflection-heavy, or loosely-typed code.

Clone this wiki locally