Skip to content

fix memory leak issue#237

Merged
yilmaztayfun merged 1 commit into
masterfrom
f/script-memoru-leak-fix
Dec 17, 2025
Merged

fix memory leak issue#237
yilmaztayfun merged 1 commit into
masterfrom
f/script-memoru-leak-fix

Conversation

@middt
Copy link
Copy Markdown
Contributor

@middt middt commented Dec 17, 2025

Summary by Sourcery

Introduce a memory-safe C# script evaluator that uses collectible assembly load contexts and shared caching to prevent memory leaks from dynamic script compilation, and wire it into the application’s scripting services.

Bug Fixes:

  • Prevent memory leaks from dynamically compiled C# scripts by loading assemblies into collectible AssemblyLoadContexts and providing cache invalidation APIs.

Enhancements:

  • Replace per-call script compilation with a cache of compiled script types keyed by a stable hash of code and configuration.
  • Centralize and cache default Roslyn metadata references to avoid repeated discovery of loaded assemblies.
  • Expose methods to clear or selectively invalidate cached scripts and report the number of cached script types.
  • Inject the C# evaluator as a singleton into the script engine so its cache is shared application-wide.

Summary by CodeRabbit

  • New Features

    • Added script cache management capabilities: clear entire cache, invalidate specific scripts, and monitor cached entry count.
    • Enhanced memory efficiency with automatic unloading of compiled script assemblies to prevent memory leaks.
  • Refactor

    • Refactored script evaluation to use dependency injection for improved maintainability and testability.

✏️ Tip: You can customize this high-level summary in your review settings.

@middt middt requested review from a team and tsimsekburgan December 17, 2025 07:08
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Dec 17, 2025

Reviewer's Guide

Refactors the C# script evaluator to cache compiled types using a collectible AssemblyLoadContext, adds cache management APIs, wires the evaluator into DI as a singleton, and updates ScriptEngine to use the injected evaluator to prevent memory leaks from dynamic script compilation.

Sequence diagram for script compilation with caching and collectible load context

sequenceDiagram
    actor Caller
    participant ScriptEngine
    participant CSharpEvaluator
    participant TypeCache as Type_cache
    participant ScriptAssemblyLoadContext as LoadContext
    participant Assembly

    Caller->>ScriptEngine: ExecuteScriptAsync
    ScriptEngine->>CSharpEvaluator: CompileToInstanceAsync_T_(code, extraReferences, usingDirectives, cancellationToken)

    CSharpEvaluator->>CSharpEvaluator: GenerateCacheKey(code, targetType, extraReferences, usingDirectives)
    CSharpEvaluator->>TypeCache: TryGetValue(cacheKey)
    alt cache_hit
        TypeCache-->>CSharpEvaluator: (Context, CompiledType)
        CSharpEvaluator->>CSharpEvaluator: Activator.CreateInstance(CompiledType)
        CSharpEvaluator-->>ScriptEngine: instance
        ScriptEngine-->>Caller: result
    else cache_miss
        CSharpEvaluator->>CSharpEvaluator: Parse syntax, add usings
        CSharpEvaluator->>CSharpEvaluator: DefaultMetadataReferences.Value
        CSharpEvaluator->>CSharpEvaluator: Create CSharpCompilation
        CSharpEvaluator->>CSharpEvaluator: compilation.Emit(memoryStream)
        alt emit_failed
            CSharpEvaluator-->>ScriptEngine: throw InvalidOperationException
            ScriptEngine-->>Caller: error
        else emit_success
            CSharpEvaluator->>LoadContext: new ScriptAssemblyLoadContext(assemblyName)
            CSharpEvaluator->>LoadContext: LoadFromStream(memoryStream)
            LoadContext-->>Assembly: loaded_assembly
            CSharpEvaluator->>Assembly: GetTypes()
            CSharpEvaluator->>CSharpEvaluator: Find type implementing T
            alt type_not_found
                CSharpEvaluator->>LoadContext: Unload()
                CSharpEvaluator-->>ScriptEngine: throw InvalidOperationException
                ScriptEngine-->>Caller: error
            else type_found
                CSharpEvaluator->>TypeCache: TryAdd(cacheKey, (LoadContext, matchedType))
                CSharpEvaluator->>CSharpEvaluator: Activator.CreateInstance(matchedType)
                CSharpEvaluator-->>ScriptEngine: instance
                ScriptEngine-->>Caller: result
            end
        end
    end
Loading

Class diagram for updated scripting evaluator and engine

classDiagram
    class IEvaluator
    class IScriptEngine

    class CSharpEvaluator {
        - ConcurrentDictionary~string,(AssemblyLoadContext,Type)~ _typeCache
        - static Lazy~IReadOnlyList~MetadataReference~~ DefaultMetadataReferences
        + int CachedTypeCount
        + Task~T~ CompileToInstanceAsync_T_(string code, IEnumerable~MetadataReference~ extraReferences, IEnumerable~string~ usingDirectives, CancellationToken cancellationToken)
        - Task~T~ CompileAndCacheAsync_T_(string code, string cacheKey, IEnumerable~MetadataReference~ extraReferences, IEnumerable~string~ usingDirectives, CancellationToken cancellationToken)
        - static string GenerateCacheKey(string code, Type targetType, IEnumerable~MetadataReference~ extraReferences, IEnumerable~string~ usingDirectives)
        - static IReadOnlyList~MetadataReference~ CreateDefaultReferences()
        + void ClearCache()
        + bool InvalidateScript_T_(string code, IEnumerable~MetadataReference~ extraReferences, IEnumerable~string~ usingDirectives)
        - static ScriptOptions CreateDefaultOptions(Func~ScriptOptions,ScriptOptions~ configureScriptOptions)
    }

    class ScriptAssemblyLoadContext {
        + ScriptAssemblyLoadContext(string name)
        # Assembly Load(AssemblyName assemblyName)
    }

    class ScriptEngine {
        - IEvaluator _evaluator
        + ScriptEngine(IEvaluator evaluator, IWorkflowMetrics workflowMetrics)
        %% other IScriptEngine members not shown
    }

    IEvaluator <|.. CSharpEvaluator
    AssemblyLoadContext <|-- ScriptAssemblyLoadContext
    IScriptEngine <|.. ScriptEngine

    ScriptEngine --> IEvaluator : uses
    CSharpEvaluator --> ScriptAssemblyLoadContext : creates
    CSharpEvaluator ..> MetadataReference : uses
    CSharpEvaluator ..> ScriptOptions : uses
    CSharpEvaluator ..> IWorkflowMetrics : uses_in_scripts
Loading

File-Level Changes

Change Details Files
Refactor CSharpEvaluator to cache compiled types and use collectible AssemblyLoadContext for unloadable dynamic script assemblies.
  • Replace static Script cache with an instance-level ConcurrentDictionary mapping a generated hash key to (AssemblyLoadContext, compiled Type).
  • Introduce GenerateCacheKey to build a stable SHA256-based key from script code, target type, using directives, and extra references.
  • Use a lazily-initialized, reusable list of MetadataReference objects instead of rebuilding references for every compilation and enable Release optimization on the Roslyn compilation.
  • Load compiled assemblies into a custom ScriptAssemblyLoadContext, locate the type implementing the requested generic type, cache it, and create instances from the cached type on subsequent calls.
  • Add ClearCache and InvalidateScript APIs to unload collectible AssemblyLoadContext instances and remove entries from the type cache.
  • Improve error reporting and validation (Environment.NewLine for diagnostics, guard against empty code).
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs
Register CSharpEvaluator as a singleton IEvaluator in the DI container and update ScriptEngine to use DI instead of constructing the evaluator directly.
  • Add CSharpEvaluator to the AddScriptingServices registration as a singleton implementation of IEvaluator, reusing the evaluator and its caches across the app.
  • Inject IEvaluator into ScriptEngine via constructor and remove the internal new CSharpEvaluator() instantiation so ScriptEngine relies on the DI-provided singleton evaluator.
src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs
src/BBT.Workflow.Application/Scripting/ScriptEngine.cs
Introduce a dedicated collectible ScriptAssemblyLoadContext used to host dynamically compiled script assemblies.
  • Create ScriptAssemblyLoadContext as an internal sealed subclass of AssemblyLoadContext configured as collectible for GC-based unloading of script assemblies.
  • Override Load to return null and fall back to the default resolution mechanism, allowing scripts to use the main application’s assemblies without custom resolution logic.
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/ScriptAssemblyLoadContext.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 17, 2025

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'review'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Introduces memory-safe, cache-backed C# script evaluation with collectible AssemblyLoadContext support. Adds SHA-256–keyed type caching and assembly unloading capabilities to CSharpEvaluator. Registers IEvaluator as a singleton in dependency injection and updates ScriptEngine to use injected evaluator instead of hardcoding instantiation.

Changes

Cohort / File(s) Summary
Cache-backed evaluator with unloadable assemblies
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs, modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/ScriptAssemblyLoadContext.cs
Implements persistent type cache keyed by SHA-256 hash of code and configuration; adds collectible AssemblyLoadContext for memory-safe assembly unloading. Introduces public cache management methods (ClearCache(), InvalidateScript<T>()), property CachedTypeCount, and supporting helpers (GenerateCacheKey(), CreateDefaultReferences(), CreateDefaultOptions()). Adds new ScriptAssemblyLoadContext class enabling deterministic assembly cleanup.
Dependency injection and evaluator registration
src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs, src/BBT.Workflow.Application/Scripting/ScriptEngine.cs
Registers IEvaluator as singleton (CSharpEvaluator) in DI container with updated documentation. Updates ScriptEngine constructor to accept injected IEvaluator instead of hardcoding instantiation; modifies XML documentation.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant ScriptEngine
    participant CSharpEvaluator
    participant Cache as Type Cache
    participant CompilationSystem as Compilation System
    participant SALC as ScriptAssemblyLoadContext
    
    Client->>ScriptEngine: Execute script
    ScriptEngine->>CSharpEvaluator: CompileToInstanceAsync<T>(code, ...)
    
    CSharpEvaluator->>CSharpEvaluator: GenerateCacheKey(code, config)
    CSharpEvaluator->>Cache: Check for cached type
    
    alt Cache Hit
        Cache-->>CSharpEvaluator: Return (ALContext, Type)
        CSharpEvaluator-->>CSharpEvaluator: Create instance from cached type
    else Cache Miss
        CSharpEvaluator->>CSharpEvaluator: CreateDefaultReferences()
        CSharpEvaluator->>CompilationSystem: Compile script to assembly
        CompilationSystem-->>CSharpEvaluator: Assembly bytecode
        
        CSharpEvaluator->>SALC: new ScriptAssemblyLoadContext(name)
        CSharpEvaluator->>SALC: Load compiled assembly (collectible)
        SALC-->>CSharpEvaluator: Loaded Assembly
        
        CSharpEvaluator->>CSharpEvaluator: Locate Type implementing T
        CSharpEvaluator->>Cache: Store (SALC, Type)
        CSharpEvaluator-->>CSharpEvaluator: Create instance from new type
    end
    
    CSharpEvaluator-->>ScriptEngine: Instance<T>
    ScriptEngine-->>Client: Execution result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Areas requiring extra attention:

  • Cache key generation logic (GenerateCacheKey()) — ensure SHA-256 hashing is deterministic and order-insensitive across code, configuration, and directive combinations
  • AssemblyLoadContext lifecycle management in CompileAndCacheAsync<T>() — verify proper unload semantics when assembly loading fails or when cached contexts are discarded via ClearCache()/InvalidateScript<T>()
  • Metadata reference caching (DefaultMetadataReferences) — confirm Lazy initialization is thread-safe and references are stable across compilations
  • Dependency injection wiring — ensure singleton IEvaluator registration does not introduce unintended state sharing or lifecycle issues

Poem

🐰 Hop, compile, and cache with care,
Assemblies unload through the air,
Memory-safe scripts now shine so bright,
Collectible contexts make it right!
Dependency injection, clean and spry—
Performance soars, no leaks nearby!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix memory leak issue' is directly related to the main changeset, which introduces memory-safe caching and collectible AssemblyLoadContext for unloading compiled assemblies to prevent memory leaks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch f/script-memoru-leak-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @middt, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical memory leak issue within the dynamic C# script evaluation component. By adopting Collectible AssemblyLoadContext and implementing a sophisticated caching strategy, the system can now efficiently compile and execute C# scripts. This change ensures that dynamically loaded assemblies are properly unloaded from memory when they are no longer referenced, significantly enhancing the application's long-term stability and optimizing resource utilization, particularly in environments with frequent script execution.

Highlights

  • Memory-Safe Script Evaluation: Implemented a memory-safe C# script evaluator by utilizing Collectible AssemblyLoadContext to ensure dynamically compiled assemblies can be unloaded from memory, effectively preventing memory leaks.
  • Script Type Caching: Introduced a robust caching mechanism for compiled script types, indexed by a unique hash of the script code and configuration. This allows for efficient reuse of already compiled scripts and their associated assemblies.
  • Explicit Cache Management: Added public methods ClearCache and InvalidateScript to allow for explicit management of the compiled script cache, enabling the unloading of specific or all cached script assemblies when their definitions change or memory needs to be reclaimed.
  • Dependency Injection Refactoring: Refactored the ScriptEngine to depend on an injected IEvaluator interface, with CSharpEvaluator registered as a singleton. This ensures that the caching mechanism is shared across the application and managed centrally.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented Dec 17, 2025

Here's the code health analysis summary for commits c2a244f..9617abb. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource C# LogoC#❌ Failure
❗ 1 occurence introduced
View Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • The new _typeCache in CSharpEvaluator is unbounded and only cleared via explicit ClearCache/InvalidateScript calls; consider adding an internal eviction strategy (e.g., size cap or LRU) to avoid trading one long-lived memory issue for another when many distinct scripts are compiled over time.
  • Because DefaultMetadataReferences is built once from the assemblies loaded at first access, any assemblies loaded later into the AppDomain will not be seen by the evaluator; if late-loaded assemblies are a realistic scenario, you may want a refresh mechanism or a way to inject additional default references.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `_typeCache` in `CSharpEvaluator` is unbounded and only cleared via explicit `ClearCache`/`InvalidateScript` calls; consider adding an internal eviction strategy (e.g., size cap or LRU) to avoid trading one long-lived memory issue for another when many distinct scripts are compiled over time.
- Because `DefaultMetadataReferences` is built once from the assemblies loaded at first access, any assemblies loaded later into the AppDomain will not be seen by the evaluator; if late-loaded assemblies are a realistic scenario, you may want a refresh mechanism or a way to inject additional default references.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses a memory leak issue by introducing a new CSharpEvaluator that utilizes collectible AssemblyLoadContexts. The implementation includes a robust caching mechanism for compiled scripts, which enhances performance by avoiding redundant compilations. The transition to using dependency injection for the evaluator is a solid design choice that promotes better testability and a cleaner architecture. My review includes a few suggestions to improve code clarity, robustness, and maintainability by refining comments and handling exceptions more explicitly.

Comment on lines +195 to +198
catch
{
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This catch block is very broad and swallows all exceptions silently, which can hide underlying issues and make debugging difficult. For example, if an assembly file is corrupted or has permission issues, it will fail silently. It's better to catch more specific exceptions that you expect MetadataReference.CreateFromFile to throw (like IOException, NotSupportedException). If logging is not feasible in this static context, consider adding a comment explaining why exceptions are being ignored.

Comment on lines +215 to +222
try
{
cached.Context.Unload();
}
catch
{
// Ignore unload failures
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The try-catch block around cached.Context.Unload() is unnecessary. According to the official .NET documentation, the AssemblyLoadContext.Unload() method does not throw any exceptions. You can safely remove this try-catch block to simplify the code.

                    cached.Context.Unload();

Comment on lines +240 to +247
try
{
cached.Context.Unload();
}
catch
{
// Ignore
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the ClearCache method, this try-catch block is unnecessary because AssemblyLoadContext.Unload() does not throw exceptions. Removing it will make the code cleaner.

            cached.Context.Unload();

{
services.TryAddSingleton<IScriptContextFactory, ScriptContextFactory>();

// Evaluator is stateless - singleton for efficiency (caches MetadataReferences only)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This comment is slightly inaccurate. CSharpEvaluator is not stateless; it maintains a cache of compiled script types in _typeCache. The main reason for its singleton registration is to share this cache across the application. The metadata references are cached in a static field, so they would be shared regardless of the instance's lifetime. A more precise comment would improve clarity.

        // The evaluator is thread-safe and manages a cache of compiled scripts.
        // A singleton lifetime ensures this cache is shared throughout the application, improving performance.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs (1)

71-142: Critical: Handle cache race condition to prevent context leak.

Lines 120-138 create a collectible AssemblyLoadContext and attempt to cache it. However, if TryAdd at line 138 returns false (another thread cached the same key first), the current thread's loadContext is neither cached nor unloaded, resulting in a leaked context that cannot be garbage collected.

Apply this fix to unload the context if caching fails:

-        // Cache the type for future reuse (same script = same assembly = same type)
-        _typeCache.TryAdd(cacheKey, (loadContext, matchedType));
+        // Cache the type for future reuse (same script = same assembly = same type)
+        if (!_typeCache.TryAdd(cacheKey, (loadContext, matchedType)))
+        {
+            // Another thread cached this script first - unload our context and use theirs
+            try
+            {
+                loadContext.Unload();
+            }
+            catch
+            {
+                // Ignore unload failures
+            }
+            
+            // Get the cached version that won the race
+            var cached = _typeCache[cacheKey];
+            return Task.FromResult((T)Activator.CreateInstance(cached.CompiledType)!);
+        }
🧹 Nitpick comments (2)
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs (2)

209-225: Document blocking behavior and concurrency semantics.

The implementation is thread-safe for concurrent calls, but AssemblyLoadContext.Unload() may block if assemblies are still referenced. Consider documenting:

  1. This method may block until assemblies can be unloaded
  2. New compilations can occur concurrently with clearing
  3. Clearing is best-effort—active scripts may prevent immediate unload

Consider adding XML documentation:

 /// <summary>
 /// Clears all cached types and unloads their assemblies.
 /// Call this to reclaim memory if script definitions change.
+/// Note: This method may block if cached assemblies are still in use.
+/// Concurrent compilations are allowed and will add new cache entries.
 /// </summary>
 public void ClearCache()

231-252: LGTM! Clean targeted invalidation API.

The implementation correctly invalidates specific scripts by matching the cache key generation logic. The generic parameter <T> is necessary since the same script code targeting different types produces different cache entries.

Consider exposing InvalidateScript<T> and ClearCache on the IEvaluator interface to enable cache management in tests and support cache eviction policies without depending on the concrete implementation.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2a244f and 9617abb.

📒 Files selected for processing (4)
  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs (3 hunks)
  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/ScriptAssemblyLoadContext.cs (1 hunks)
  • src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs (2 hunks)
  • src/BBT.Workflow.Application/Scripting/ScriptEngine.cs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.cs

📄 CodeRabbit inference engine (.cursor/rules/vnext.mdc)

**/*.cs: Write concise, idiomatic C# code with accurate examples
Follow Aether Framework's recommended folder and module structure (*.Application, *.Domain, *.Infrastructure, *.HttpApi)
Use object-oriented and functional programming patterns as appropriate
Prefer LINQ and lambda expressions for collection operations
Use descriptive variable and method names (e.g., IsUserSignedIn, CalculateTotal)
Follow Microsoft's modular development approach with extension structure to separate concerns between layers (Application, Domain, Infrastructure, etc.)
Organize extensions following the namespace of the class or structure where the extension will be written, generally in BBT.Workflow.Domain project to minimize 'using' statements
Follow Clean Architecture and SOLID principles
Apply Domain-Driven Design patterns: Aggregates, Entities, ValueObjects, Repositories, Domain Events
All cross-cutting concerns such as Clock, GuidGenerator, Mapper, Tracing, Logging, Metrics MUST use Aether SDK
Workflow, orchestration, task handling and runtime logic MUST follow vNext architectural conventions
Always use async/await
Use Dependency Injection everywhere
Avoid leaking EF entities
Apply Result Pattern
Use PascalCase for class names, method names, and public members
Use camelCase for local variables and private fields
Use UPPERCASE for constants
Prefix interface names with 'I' (e.g., IUserService)
Use C# 10+ features when appropriate (e.g., record types, pattern matching, null-coalescing assignment)
Leverage built-in ASP.NET Core features and middleware, as well as Aether's modules and features
Use Entity Framework Core effectively for database operations, integrating with Aether's AetherDbContext and repository abstractions
Follow the C# Coding Conventions from Microsoft documentation
Use C#'s expressive syntax (e.g., null-conditional operators, string interpolation)
Use 'var' for implicit typing when the type is obvious
Use exceptions for exceptional cases, not for control flow
Implemen...

Files:

  • src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs
  • src/BBT.Workflow.Application/Scripting/ScriptEngine.cs
  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs
  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/ScriptAssemblyLoadContext.cs
🧠 Learnings (10)
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Use BackgroundService for task processors and IHostedService for lifecycle control

Applied to files:

  • src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Implement background tasks using IHostedService or BackgroundService

Applied to files:

  • src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Organize extensions following the namespace of the class or structure where the extension will be written, generally in BBT.Workflow.Domain project to minimize 'using' statements

Applied to files:

  • src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Use Dependency Injection everywhere

Applied to files:

  • src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Always use IDistributedCache for caching strategies instead of IMemoryCache

Applied to files:

  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Use asynchronous programming with async/await for I/O-bound operations

Applied to files:

  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Use C# 10+ features when appropriate (e.g., record types, pattern matching, null-coalescing assignment)

Applied to files:

  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Leverage built-in ASP.NET Core features and middleware, as well as Aether's modules and features

Applied to files:

  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Use C#'s expressive syntax (e.g., null-conditional operators, string interpolation)

Applied to files:

  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs
📚 Learning: 2025-12-15T22:59:58.153Z
Learnt from: CR
Repo: burgan-tech/vnext PR: 0
File: .cursor/rules/vnext.mdc:0-0
Timestamp: 2025-12-15T22:59:58.153Z
Learning: Applies to **/*.cs : Write concise, idiomatic C# code with accurate examples

Applied to files:

  • modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs
🧬 Code graph analysis (2)
src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs (2)
src/BBT.Workflow.Domain/Scripting/Factory/Services/ScriptContextFactory.cs (1)
  • ScriptContextFactory (11-23)
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs (1)
  • CSharpEvaluator (24-262)
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs (3)
src/BBT.Workflow.Application/Scripting/ScriptEngine.cs (1)
  • Task (82-162)
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/IEvaluator.cs (1)
  • Task (10-14)
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/ScriptAssemblyLoadContext.cs (2)
  • ScriptAssemblyLoadContext (11-31)
  • ScriptAssemblyLoadContext (17-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Sourcery review
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Build and analyze
🔇 Additional comments (7)
src/BBT.Workflow.Application/Microsoft/Extensions/DependencyInjection/TaskServiceCollectionExtensions.cs (2)

2-2: LGTM! Clear documentation of memory management strategy.

The added documentation accurately describes how the collectible AssemblyLoadContext prevents memory leaks from dynamic script compilation by enabling garbage collection of assemblies when no longer referenced.

Also applies to: 158-159


164-167: Verify caching approach against coding guidelines.

The singleton registration is appropriate for a stateless evaluator. However, the comment "caches MetadataReferences only" is incomplete—the evaluator also caches compiled Types in an in-memory ConcurrentDictionary (see CSharpEvaluator._typeCache at line 30).

Per coding guidelines: "Always use IDistributedCache for caching strategies instead of IMemoryCache." The current implementation uses in-memory caching, which may not be suitable in distributed/multi-instance scenarios where cache invalidation and sharing would be beneficial.

Consider whether:

  1. Script cache should be shared across multiple application instances via IDistributedCache
  2. The current in-memory approach is acceptable given that compiled assemblies cannot be serialized and distributed
  3. Cache eviction/invalidation strategies are sufficient for long-running processes

Based on coding guidelines, the in-memory cache approach should be justified if it deviates from the IDistributedCache requirement.

src/BBT.Workflow.Application/Scripting/ScriptEngine.cs (1)

17-27: LGTM! Proper dependency injection pattern.

The refactor from hardcoded new CSharpEvaluator() to constructor injection follows the "Use Dependency Injection everywhere" coding guideline and improves testability and lifecycle management.

modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/ScriptAssemblyLoadContext.cs (1)

1-31: LGTM! Clean and correct collectible context implementation.

The implementation is minimal and correct:

  • The isCollectible: true flag enables assembly unloading, which is the core mechanism for preventing memory leaks
  • Returning null from Load delegates to default resolution, allowing scripts to reuse assemblies from the main application without duplication
  • The unique name generation with Guid aids in debugging and diagnostics
modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs (3)

19-42: LGTM! Thread-safe cache implementation with proper context tracking.

The implementation correctly uses:

  • ConcurrentDictionary for thread-safe cache operations
  • Lazy<T> with ExecutionAndPublication mode for thread-safe initialization of default references
  • Tuple storage of (AssemblyLoadContext, Type) to enable proper unloading

The CachedTypeCount property provides useful observability for monitoring cache growth.


147-179: LGTM! Robust cache key generation with proper collision resistance.

The implementation correctly produces stable, unique cache keys by:

  • Using AssemblyQualifiedName to distinguish types including version info
  • Sorting collections with OrderBy to ensure order independence
  • Using SHA-256 for strong collision resistance

The use of MetadataReference.Display (typically file paths) should be stable across executions given consistent assembly locations.


185-203: LGTM! Defensive assembly probing with proper error handling.

The implementation correctly:

  • Filters dynamic assemblies that cannot have metadata references
  • Uses try-catch to handle inaccessible assemblies gracefully
  • Leverages lazy initialization to compute references once

The empty catch block at line 195 is acceptable for this probing scenario where continuing despite individual failures is the desired behavior.

Comment on lines 45 to +66
public Task<T> CompileToInstanceAsync<T>(
string code,
IEnumerable<MetadataReference>? extraReferences = null,
IEnumerable<string>? usingDirectives = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("Code cannot be null or empty", nameof(code));

// Generate cache key from code + target type + configuration
var cacheKey = GenerateCacheKey(code, typeof(T), extraReferences, usingDirectives);

// Try to get cached type and create instance from it
if (_typeCache.TryGetValue(cacheKey, out var cached))
{
var instance = (T)Activator.CreateInstance(cached.CompiledType)!;
return Task.FromResult(instance);
}

// Compile, cache the type, and return instance
return CompileAndCacheAsync<T>(code, cacheKey, extraReferences, usingDirectives, cancellationToken);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the CSharpEvaluator file and related interface files
find . -name "*Evaluator*" -type f | head -20

# Find the IEvaluator interface
find . -name "*IEvaluator*" -type f

# Search for CompileAndCacheAsync method
rg -l "CompileAndCacheAsync" --type=cs

Repository: burgan-tech/vnext

Length of output: 963


🏁 Script executed:

#!/bin/bash
# Read the IEvaluator interface to see if it documents constructor requirements
cat -n ./modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/IEvaluator.cs

echo "---"

# Read the CSharpEvaluator file to examine implementation details
wc -l ./modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs

Repository: burgan-tech/vnext

Length of output: 696


🏁 Script executed:

#!/bin/bash
# Read the entire CSharpEvaluator file to understand the implementation
cat -n ./modules/BBT.Workflow.Modules.Scripting/BBT/Workflow/Scripting/Evaluators/CSharpEvaluator.cs

Repository: burgan-tech/vnext

Length of output: 11093


🏁 Script executed:

#!/bin/bash
# Search for tests related to CSharpEvaluator to understand expected behavior
find . -name "*Test*" -o -name "*test*" | grep -i evaluator

# Search for any usage of CompileToInstanceAsync in the codebase
rg "CompileToInstanceAsync" --type=cs -B 2 -A 5

# Search for Activator.CreateInstance patterns in the codebase
rg "Activator\.CreateInstance" --type=cs -B 2 -A 2

Repository: burgan-tech/vnext

Length of output: 40513


🏁 Script executed:

#!/bin/bash
# Check if there are any tests specifically for constructor validation
rg "MissingMethodException" --type=cs

# Check test implementations to see if they ever test with parameterized constructors
rg "public class.*:" test/BBT.Workflow.Application.Tests/Scripting/ --type=cs -A 3 | head -40

# Look for any try-catch around Activator.CreateInstance patterns
rg "try.*Activator\.CreateInstance|catch.*MissingMethodException" --type=cs -B 2 -A 3

Repository: burgan-tech/vnext

Length of output: 3645


🏁 Script executed:

#!/bin/bash
# Look at ScriptValidationTests to see what validation is performed
cat -n test/BBT.Workflow.Application.Tests/Scripting/ScriptValidationTests.cs | head -100

echo "---"

# Look for any test cases that might test with a constructor that has parameters
rg "public class.*\(" test/BBT.Workflow.Application.Tests/Scripting/ --type=cs -A 1

Repository: burgan-tech/vnext

Length of output: 4162


🏁 Script executed:

#!/bin/bash
# Check all test classes to see if any test with parameterized constructors
find test/BBT.Workflow.Application.Tests/Scripting -name "*.cs" -exec grep -l "public.*(" {} \;

# Look at a specific test to see the pattern of classes being compiled
cat -n test/BBT.Workflow.Application.Tests/Scripting/ScriptEngineTests.cs | head -150

Repository: burgan-tech/vnext

Length of output: 6817


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that explicitly test error handling for constructor issues
rg "MissingMethodException|parameterless|constructor" test/BBT.Workflow.Application.Tests/Scripting/ --type=cs

# Check if there's any exception handling around the instance creation
cat -n src/BBT.Workflow.Application/Scripting/ScriptEngine.cs | head -100

Repository: burgan-tech/vnext

Length of output: 5543


🏁 Script executed:

#!/bin/bash
# Read the rest of ScriptEngine.cs to see exception handling
cat -n src/BBT.Workflow.Application/Scripting/ScriptEngine.cs | tail -100

Repository: burgan-tech/vnext

Length of output: 5279


🏁 Script executed:

#!/bin/bash
# Final verification: Check if MissingMethodException is handled anywhere in the call chain
rg "MissingMethodException|catch.*Exception.*Activator" --type=cs

# Verify the exact behavior of Activator.CreateInstance by looking at any documentation
rg "Activator\.CreateInstance.*System.Reflection" --type=cs

Repository: burgan-tech/vnext

Length of output: 43


Document and validate parameterless constructor requirement for compiled types.

Line 60 uses Activator.CreateInstance(cached.CompiledType) which requires a parameterless constructor. If compiled code defines a type with only parameterized constructors, this throws MissingMethodException at runtime—caught only as a generic exception, not a domain-specific error.

Add:

  1. XML documentation to CompileToInstanceAsync<T> specifying that compiled types must have a parameterless constructor
  2. Validation during compilation to verify the matched type has a parameterless constructor (inspect via reflection)
  3. Catch and wrap MissingMethodException with a clear error message indicating the requirement

This ensures developers get actionable feedback instead of cryptic reflection errors.

@yilmaztayfun yilmaztayfun merged commit 3f7992f into master Dec 17, 2025
6 of 8 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Dec 19, 2025
Merged
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.

3 participants