Skip to content

Support derived activity descriptor customization#7422

Open
Copilot wants to merge 4 commits into
mainfrom
copilot/support-custom-activity-ports
Open

Support derived activity descriptor customization#7422
Copilot wants to merge 4 commits into
mainfrom
copilot/support-custom-activity-ports

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 25, 2026

Purpose

Allow custom activities derived from existing activities to define their own descriptor identity and customize inputs, outputs, ports, and outcomes without inheriting conflicting base metadata.


Scope

Select one primary concern:

  • Bug fix (behavior change)
  • Refactor (no behavior change)
  • Documentation update
  • Formatting / code cleanup
  • Dependency / build update
  • New feature

If this PR includes multiple unrelated concerns, please split it before requesting review.


Description

Problem

Derived activity classes inherited base activity metadata during descriptor/type-name generation. This caused custom activity types copied or derived from built-ins to collide with the base activity identity and prevented custom descriptor aspects from being reliably surfaced.

Solution

  • Concrete activity identity

    • ActivityTypeNameHelper now reads ActivityAttribute only when declared on the concrete type.
    • A derived activity without its own [Activity] gets its own namespace/type name instead of inheriting the base activity identity.
  • Derived descriptor metadata

    • ActivityDescriber now uses non-inherited activity/display/category/description attributes.
    • Derived classes can intentionally override descriptor metadata by declaring their own attributes.
  • Derived property customization

    • Descriptor reflection now prefers properties declared closest to the concrete type.
    • Hidden/customized inputs, outputs, and embedded ports on derived activities win over base properties.

Example:

public class MyHttpRequest : SendHttpRequest
{
    [Input]
    public new Input<ICollection<string>> ExpectedStatusCodes { get; set; } = null!;
}

MyHttpRequest is now described as its own activity type, with its derived input metadata preferred.


Verification

Steps:

  1. Review ActivityDescriberTests.
  2. Run dotnet test test/unit/Elsa.Workflows.Core.UnitTests/Elsa.Workflows.Core.UnitTests.csproj /p:RestoreIgnoreFailedSources=true.
  3. Confirm derived activity descriptors use concrete identity and derived property metadata.

Expected outcome:

Derived activity descriptors no longer inherit base activity identity unless explicitly declared, and derived inputs/outputs/ports/outcomes are reflected correctly.


Screenshots / Recordings (if applicable)

N/A


Commit Convention

We recommend using conventional commit prefixes:

  • fix: – Bug fixes (behavior change)
  • feat: – New features
  • refactor: – Code changes without behavior change
  • docs: – Documentation updates
  • chore: – Maintenance, tooling, or dependency updates
  • test: – Test additions or modifications

Clear commit messages make reviews easier and history more meaningful.


Checklist

  • The PR is focused on a single concern
  • Commit messages follow the recommended convention
  • Tests added or updated (if applicable)
  • Documentation updated (if applicable)
  • No unrelated cleanup included
  • All tests pass

Copilot AI linked an issue Apr 25, 2026 that may be closed by this pull request
Copilot AI and others added 2 commits April 25, 2026 12:51
Copilot AI changed the title [WIP] Explore feasibility of supporting classes derived from activities Support derived activity descriptor customization Apr 25, 2026
Copilot AI requested a review from sfmskywalker April 25, 2026 12:57
@sfmskywalker sfmskywalker marked this pull request as ready for review April 25, 2026 14:32
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 25, 2026

Greptile Summary

This PR fixes descriptor inheritance for derived activity classes by switching all GetCustomAttribute calls to inherit: false and introducing a GetPublicInstanceProperties helper that prefers the most-derived property declaration via inheritance-distance ordering. The ActivityTypeNameHelper receives the same treatment so derived types without their own [Activity] attribute get independent namespace/type names.

Confidence Score: 5/5

Safe to merge; no P0/P1 issues found — only minor consistency gaps in inherited attribute handling.

All findings are P2. The core logic of GetPublicInstanceProperties and the inherit: false changes is correct and well-covered by new tests. The two remaining inconsistencies (BrowsableAttribute class-level and GetCustomAttributes(true)) are edge-case scenarios unlikely to affect most users.

src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs — lines 64 and 67 still use inherited attribute lookups inconsistently with the rest of the PR.

Important Files Changed

Filename Overview
src/modules/Elsa.Workflows.Core/Helpers/ActivityTypeNameHelper.cs Adds inherit: false to both GetCustomAttribute<ActivityAttribute>() calls so derived classes without their own [Activity] get their own namespace/type name; also adds a missing newline at EOF.
src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs Switches all descriptor-attribute lookups to inherit: false and introduces GetPublicInstanceProperties that sorts by inheritance distance to prefer the most-derived property declaration; BrowsableAttribute (class-level, line 64) and GetCustomAttributes(true) (line 67) still inherit from base types inconsistently.
test/unit/Elsa.Workflows.Core.UnitTests/Services/ActivityDescriberTests.cs New test file with four focused tests covering: derived type identity, constructor identity, property deduplication (input/output/embedded ports), customized flow-node outcomes, and non-inheritance of base flow ports. Good coverage of the core scenarios.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[DescribeActivityAsync] --> B{inherit:false lookup\nActivityAttribute}
    B -->|found on concrete type| C[Use declared namespace / type / version / display / category / description / kind]
    B -->|not found| D[Derive from type name & CLR namespace]

    A --> E[GetPublicInstanceProperties]
    E --> F[GetProperties Public+Instance]
    F --> G[OrderBy InheritanceDistance\nclosest to concrete first]
    G --> H[DistinctBy Name\nderived property wins]

    H --> I[Filter: Input properties]
    H --> J[Filter: Output properties]
    H --> K[Filter: Embedded port properties\nIActivity assignable]

    B2{inherit:false lookup\nFlowNodeAttribute} --> L[Flow ports from outcomes]
    B2 -->|not found| M[No flow ports]

    A --> B2
    I --> N[ActivityDescriptor]
    J --> N
    K --> N
    L --> N
    M --> N
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs
Line: 64

Comment:
**`BrowsableAttribute` class-level still inherits from base types**

Line 64 still calls `GetCustomAttribute<BrowsableAttribute>()` with the default `inherit: true`, while every other descriptor attribute in this method (`ActivityAttribute`, `DisplayNameAttribute`, `CategoryAttribute`, `DescriptionAttribute`, `FlowNodeAttribute`) was explicitly changed to `inherit: false`. A derived class without its own `[Browsable]` annotation will inherit the base class's visibility setting — for example, if the base activity is `[Browsable(false)]`, the derived type becomes invisibly non-browsable even when the author intends it to be a distinct, visible activity.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs
Line: 67

Comment:
**`GetCustomAttributes(true)` still collects inherited `OutputAttribute`**

`GetCustomAttributes(true)` on line 67 retrieves all attributes from the type hierarchy. The `OutputAttribute` extracted from this list on line 68 controls the `IsSerializable` flag of the default output descriptor. A derived class without its own `[Output(...)]` class-level annotation silently inherits the base class's `IsSerializable` value, which is inconsistent with the non-inherited treatment applied to every other descriptor attribute in this PR.

```suggestion
        var attributes = activityType.GetCustomAttributes(false).Cast<Attribute>().ToList();
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "fix: use inherit: false for FlowNodeAttr..." | Re-trigger Greptile

@sfmskywalker
Copy link
Copy Markdown
Member

@claude This is a comment left during a code review.
Path: src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs
Line: 49

Comment:
FlowNodeAttribute still inherits from base types

FlowNodeAttribute is the only descriptor attribute that still uses the default inherit: true semantics (line 49). Every other attribute in this method (ActivityAttribute, DisplayNameAttribute, CategoryAttribute, DescriptionAttribute) was explicitly changed to inherit: false. As a result, a derived class that does not declare its own [FlowNode] will silently inherit the base class's flow port outcomes — even though ActivityAttribute (identity), display name, category, and description are all not inherited. This inconsistency can surprise authors of derived activities who expect clean slate behaviour for all descriptor metadata.

Can you resolve this?

@claude
Copy link
Copy Markdown

claude Bot commented Apr 25, 2026

Claude finished @sfmskywalker's task in 58s —— View job


Fixing FlowNodeAttribute inheritance inconsistency

  • Gather context
  • Read ActivityDescriber.cs and understand the fix needed
  • Apply inherit: false to FlowNodeAttribute lookup (line 49)
  • Add test: derived class without [FlowNode] should get no flow ports
  • Commit and push

Done. Two changes were made:

  1. ActivityDescriber.cs line 49 — changed GetCustomAttribute<FlowNodeAttribute>() to GetCustomAttribute<FlowNodeAttribute>(false). This makes FlowNodeAttribute consistent with every other descriptor attribute lookup in the method (all already use inherit: false). A derived class without its own [FlowNode] now gets an empty flow port list instead of silently inheriting the base class's outcomes.

  2. ActivityDescriberTests.cs — added DescribeActivityAsync_DerivedActivityWithoutFlowNode_DoesNotInheritBaseFlowPorts and the companion DerivedActivityWithoutFlowNode private class to cover this previously-missing case.
    | Branch

…iber

Aligns FlowNodeAttribute lookup with all other descriptor attribute
lookups so derived activities without their own [FlowNode] get no
inherited flow ports, matching the clean-slate behaviour of Activity-,
DisplayName-, Category-, and DescriptionAttribute.

Co-authored-by: Sipke Schoorstra <sfmskywalker@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom activity with custom ports

2 participants