xaf-memory-leaks

XAF Memory Leak Prevention - event handler symmetry (OnActivated/OnDeactivated/Dispose), ObjectSpace scoped disposal with using statement, batch processing large datasets, IDisposable pattern for controllers with List<IDisposable> tracker, WeakEventSubscription, static reference anti-patterns, CollectionSource disposal, Session/HttpContext/Application anti-patterns (WebForms), ObjectSpacePool, controller lifecycle tracking, NavigationMonitor, warning signs, diagnostic tools (dotMemory, PerfView, XAF Tracing). Use when diagnosing memory leaks, auditing controller disposal, reviewing ObjectSpace lifetime, or reviewing Session usage in DevExpress XAF applications.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "xaf-memory-leaks" with this command: npx skills add kashiash/xaf-skills/kashiash-xaf-skills-xaf-memory-leaks

XAF: Memory Leak Prevention

Root Causes

CauseSymptom
Event handler not unsubscribedMemory grows with navigation; handler fires multiple times
ObjectSpace not disposedMemory grows proportional to data accessed
Static reference to instanceObjects never GC'd; growing memory profile
CollectionSource not disposedCached data retained after view closes
ObjectSpace/objects in SessionMemory scales with active user count; session timeout failures
Controller holds undisposed resourcesFinalizer queue pressure; slow GC

Event Handler Pattern — Most Common Leak

Every += in OnActivated needs a matching -= in both OnDeactivated and Dispose.

public class SafeController : ViewController {
    private bool eventsSubscribed;

    protected override void OnActivated() {
        base.OnActivated();
        if (!eventsSubscribed) {
            View.SelectionChanged += View_SelectionChanged;
            View.CurrentObjectChanged += View_CurrentObjectChanged;
            View.ObjectSpace.ObjectChanged += ObjectSpace_ObjectChanged;
            eventsSubscribed = true;
        }
    }

    protected override void OnDeactivated() {
        UnsubscribeEvents();
        base.OnDeactivated();
    }

    protected override void Dispose(bool disposing) {
        if (disposing) UnsubscribeEvents(); // safety net
        base.Dispose(disposing);
    }

    private void UnsubscribeEvents() {
        if (!eventsSubscribed) return;
        if (View != null) {
            View.SelectionChanged -= View_SelectionChanged;
            View.CurrentObjectChanged -= View_CurrentObjectChanged;
        }
        if (View?.ObjectSpace != null)
            View.ObjectSpace.ObjectChanged -= ObjectSpace_ObjectChanged;
        eventsSubscribed = false;
    }
}

Comprehensive Disposal Pattern — ResourceTrackingController

Use List<IDisposable> + List<Action> to track all resources safely:

public class ResourceTrackingController : ViewController {
    private readonly List<IDisposable> _disposables = new();
    private readonly List<Action> _cleanups = new();
    private bool _disposed;

    protected T Track<T>(T resource) where T : IDisposable {
        _disposables.Add(resource);
        return resource;
    }

    protected void AddCleanup(Action cleanup) => _cleanups.Add(cleanup);

    protected override void OnActivated() {
        base.OnActivated();
        // Subscribe and auto-track cleanup
        View.SelectionChanged += OnSelectionChanged;
        AddCleanup(() => { if (View != null) View.SelectionChanged -= OnSelectionChanged; });

        // Track a CollectionSource
        var cs = Track(new CollectionSource(ObjectSpace, typeof(MyObject)));
    }

    protected override void OnDeactivated() {
        RunCleanups();
        base.OnDeactivated();
    }

    protected override void Dispose(bool disposing) {
        if (!_disposed && disposing) {
            RunCleanups();
            foreach (var d in _disposables)
                try { d?.Dispose(); } catch { }
            _disposables.Clear();
            _disposed = true;
        }
        base.Dispose(disposing);
    }

    private void RunCleanups() {
        foreach (var action in _cleanups)
            try { action(); } catch { }
        _cleanups.Clear();
    }
}

Weak Event Subscription

For long-lived controllers that subscribe to events on short-lived objects:

public class WeakEventSubscription : IDisposable {
    private WeakReference _sourceRef;
    private readonly string _eventName;
    private readonly EventHandler _handler;

    public WeakEventSubscription(object source, string eventName, EventHandler handler) {
        _sourceRef = new WeakReference(source);
        _eventName = eventName;
        _handler = handler;
        source.GetType().GetEvent(eventName)?.AddEventHandler(source, handler);
    }

    public void Dispose() {
        var source = _sourceRef?.Target;
        if (source != null)
            source.GetType().GetEvent(_eventName)?.RemoveEventHandler(source, _handler);
        _sourceRef = null;
    }
}

// Usage in controller:
private readonly List<WeakEventSubscription> _weakSubs = new();

protected override void OnActivated() {
    base.OnActivated();
    _weakSubs.Add(new WeakEventSubscription(
        View, nameof(View.SelectionChanged), OnSelectionChanged));
}

protected override void Dispose(bool disposing) {
    if (disposing) {
        foreach (var sub in _weakSubs) sub.Dispose();
        _weakSubs.Clear();
    }
    base.Dispose(disposing);
}

ObjectSpace — Scoped Disposal

Never store IObjectSpace in a static field, singleton, or Session.

// ✅ Short-lived operation
public void ProcessData() {
    using var os = Application.CreateObjectSpace(typeof(MyObject));
    var obj = os.FindObject<MyObject>(CriteriaOperator.Parse("Name = ?", "Test"));
    obj.Status = Status.Processed;
    os.CommitChanges();
} // disposed here — tracked objects released

// ❌ Anti-patterns
private static IObjectSpace _globalOs;           // never
Session["XafObjectSpace"] = Application.CreateObjectSpace(); // never in Session

ObjectSpace — Batch Processing Large Datasets

public void ProcessAllRecords() {
    const int batchSize = 500;
    int skip = 0;

    while (true) {
        using var os = Application.CreateObjectSpace(typeof(MyObject));
        var batch = os.GetObjects<MyObject>()
            .Skip(skip).Take(batchSize).ToList();

        if (batch.Count == 0) break;

        foreach (var obj in batch)
            Process(obj);

        os.CommitChanges();
        skip += batch.Count;

        // Force GC between large batches
        if (skip % 5000 == 0) {
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
    }
}

ObjectSpace Pool (Advanced — High-Throughput Scenarios)

public class ObjectSpacePool : IDisposable {
    private readonly ConcurrentQueue<IObjectSpace> _pool = new();
    private readonly XafApplication _app;
    private readonly int _maxSize;
    private int _currentSize;

    public ObjectSpacePool(XafApplication app, int maxSize = 10) {
        _app = app; _maxSize = maxSize;
    }

    public IObjectSpace Rent() =>
        _pool.TryDequeue(out var os)
            ? (Interlocked.Decrement(ref _currentSize), os).os
            : _app.CreateObjectSpace();

    public void Return(IObjectSpace os) {
        if (os == null) return;
        if (os.IsModified) os.ReloadChangedObjects();
        if (_currentSize < _maxSize) {
            _pool.Enqueue(os);
            Interlocked.Increment(ref _currentSize);
        } else os.Dispose();
    }

    public void Dispose() {
        while (_pool.TryDequeue(out var os)) os.Dispose();
    }
}

CollectionSource Disposal

public class ListController : ViewController<ListView> {
    private CollectionSource _cs;

    protected override void OnActivated() {
        base.OnActivated();
        _cs = new CollectionSource(ObjectSpace, typeof(MyObject));
        _cs.Criteria["Date"] = CriteriaOperator.Parse(
            "CreatedOn >= ?", DateTime.Today.AddDays(-30));
        View.CollectionSource = _cs;
    }

    protected override void OnDeactivated() {
        _cs?.Dispose(); _cs = null;
        base.OnDeactivated();
    }

    protected override void Dispose(bool disposing) {
        if (disposing) { _cs?.Dispose(); _cs = null; }
        base.Dispose(disposing);
    }
}

Session / HttpContext (WebForms Only)

Anti-Patterns

// ❌ ObjectSpace in Session — lives until session expires (30 min+ of retained objects)
Session["XafObjectSpace"] = Application.CreateObjectSpace();

// ❌ Large collection in Session
Session["AllCustomers"] = objectSpace.GetObjects<Customer>();

// ❌ No cleanup on session end
protected void Application_Session_End(object sender, EventArgs e) {
    // Missing: dispose XAF objects
}

Correct Patterns

// ✅ Request-scoped ObjectSpace (disposed at end of request)
public static IObjectSpace GetRequestObjectSpace(XafApplication app) {
    var ctx = HttpContext.Current;
    if (ctx == null) return app.CreateObjectSpace();

    var os = ctx.Items["XafOs"] as IObjectSpace;
    if (os == null) {
        os = app.CreateObjectSpace();
        ctx.Items["XafOs"] = os;
        ctx.DisposeOnPipelineCompleted(os); // auto-dispose at request end
    }
    return os;
}

// ✅ Session_End cleanup — dispose all IDisposable objects stored in session
public class SessionCleanupModule : IHttpModule {
    public void Init(HttpApplication context) {
        context.Session_End += (s, e) => {
            var session = HttpContext.Current?.Session;
            if (session == null) return;
            foreach (string key in session.Keys.Cast<string>().ToList()) {
                if (session[key] is IDisposable d) {
                    try { d.Dispose(); session.Remove(key); }
                    catch (Exception ex) { Tracing.Tracer.LogError(ex.ToString()); }
                }
            }
        };
    }
    public void Dispose() { }
}

Controller Lifecycle Tracker (Diagnostic)

Detect duplicate activations or missing deactivation in development:

public class ControllerLifecycleTracker {
    private static readonly Dictionary<string, int> _active = new();

    public static void TrackActivation(Controller controller) {
        var key = controller.GetType().Name;
        _active[key] = _active.GetValueOrDefault(key, 0) + 1;
        if (_active[key] > 1)
            Tracing.Tracer.LogWarning(
                $"Multiple active instances of {key}: {_active[key]}");
    }

    public static void TrackDeactivation(Controller controller) {
        var key = controller.GetType().Name;
        if (_active.ContainsKey(key))
            if (--_active[key] <= 0) _active.Remove(key);
    }
}

// In your controller (development only):
protected override void OnActivated() {
    base.OnActivated();
    ControllerLifecycleTracker.TrackActivation(this);
}
protected override void OnDeactivated() {
    ControllerLifecycleTracker.TrackDeactivation(this);
    base.OnDeactivated();
}

Navigation Memory Monitor (Diagnostic)

public class NavigationMonitorController : WindowController {
    private static int _navCount;
    private static readonly Dictionary<string, int> _viewCounts = new();

    protected override void OnFrameAssigned() {
        base.OnFrameAssigned();
        if (Frame != null) Frame.ViewChanged += Frame_ViewChanged;
    }

    private void Frame_ViewChanged(object sender, ViewChangedEventArgs e) {
        _navCount++;
        if (e.View != null) {
            var t = e.View.GetType().Name;
            _viewCounts[t] = _viewCounts.GetValueOrDefault(t, 0) + 1;
        }

        if (_navCount % 10 == 0) {
            GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
            Tracing.Tracer.LogValue("Navigations", _navCount);
            Tracing.Tracer.LogValue("Memory after nav",
                GC.GetTotalMemory(true));
        }
    }

    protected override void Dispose(bool disposing) {
        if (disposing && Frame != null)
            Frame.ViewChanged -= Frame_ViewChanged;
        base.Dispose(disposing);
    }
}

Static Reference Anti-Patterns

// ❌ Never store controller/view/ObjectSpace in static fields
private static List<Controller> _allControllers = new();  // leak
private static IObjectSpace _sessionOs;                    // leak
private static MyController _instance;                     // leak

// ✅ Use instance fields; clean up in Dispose
private readonly List<IDisposable> _ownedResources = new();

Warning Signs

SignLikely Cause
Memory grows with each navigationEvent handlers not unsubscribed in OnDeactivated
Same handler fires multiple timesController activated multiple times without deactivation
Memory scales with user countObjectSpace or objects stored in Session
OutOfMemoryException under normal loadObjectSpace retained / large unconstrained query
GC pauses increase over timeStatic collections or long-lived ObjectSpaces
IIS app pool recyclingSession memory pressure
Session timeout failuresLarge objects (ObjectSpace) in session

Diagnostic Tools

// Enable XAF tracing to monitor ObjectSpace create/dispose
Tracing.Tracer.Initialize(TraceLevel.Verbose,
    Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\xaf_trace.log");

// Memory snapshot (development only)
var before = GC.GetTotalMemory(false);
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
Tracing.Tracer.LogValue("Retained after GC", GC.GetTotalMemory(true));

// ObjectSpace internal tracking (debug reflection)
public static void LogObjectSpaceStats(IObjectSpace os) {
    if (os is not BaseObjectSpace baseOs) return;
    var modified = typeof(BaseObjectSpace)
        .GetField("modifiedObjects", BindingFlags.NonPublic | BindingFlags.Instance)
        ?.GetValue(baseOs) as IDictionary;
    Tracing.Tracer.LogValue("Modified objects in OS", modified?.Count ?? 0);
}

Profilers:

  • dotMemory (JetBrains) — best for XAF object tracking
  • PerfView (Microsoft, free) — .NET GC and heap analysis
  • Application Insights — production memory monitoring

Code Review Checklist

✅ Every View.Event += in OnActivated has -= in OnDeactivated AND Dispose
✅ No static fields storing Controller, View, or IObjectSpace references
✅ All IObjectSpace created outside the view use `using` or explicit Dispose
✅ CollectionSource disposed in OnDeactivated and Dispose
✅ Large dataset operations use batching (new ObjectSpace per batch)
✅ No ObjectSpace stored in Session, Application, or singleton services
✅ Session_End handler disposes all IDisposable objects in Session (WebForms)
✅ Dispose(bool) calls base.Dispose(disposing) as the last statement
✅ Double-disposal safe (guard with bool _disposed)
✅ Disposal methods wrap each operation in try/catch to ensure full cleanup

Source Links

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

xaf-security

No summary provided by upstream source.

Repository SourceNeeds Review
General

xaf-office

No summary provided by upstream source.

Repository SourceNeeds Review
General

xaf-winforms-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

xaf-editors

No summary provided by upstream source.

Repository SourceNeeds Review