ApiEventManager Rework #1008

Closed
opened 2026-01-29 16:54:28 +00:00 by claunia · 43 comments
Owner

Originally created by @softworkz on GitHub (Nov 9, 2025).

Figured out the problem

internal static void AddEvent(string eventName, object id, Action callback, Action value)
{
    var call = (Action)events.GetOrAdd(eventName, callback);
    if (call == null)
    {
        BridgeConnector.Socket.On(eventName + id, () =>
        {
            ((Action)events[eventName])();
        });
        BridgeConnector.Socket.Emit($"register-{eventName}", id);
    }
    
    call += value;
    events[eventName] = call;
}

@softworkz could you advise whether it requires any locks and if so where.

Originally posted by @agracio in https://github.com/ElectronNET/Electron.NET/issues/913#issuecomment-3508416476

Originally created by @softworkz on GitHub (Nov 9, 2025). Figured out the problem ```cs internal static void AddEvent(string eventName, object id, Action callback, Action value) { var call = (Action)events.GetOrAdd(eventName, callback); if (call == null) { BridgeConnector.Socket.On(eventName + id, () => { ((Action)events[eventName])(); }); BridgeConnector.Socket.Emit($"register-{eventName}", id); } call += value; events[eventName] = call; } ``` @softworkz could you advise whether it requires any locks and if so where. _Originally posted by @agracio in https://github.com/ElectronNET/Electron.NET/issues/913#issuecomment-3508416476_
claunia added the enhancement label 2026-01-29 16:54:28 +00:00
Author
Owner

@softworkz commented on GitHub (Nov 9, 2025):

@agracio - This was much harder than I had thought and I had to try it out myself to be sure.

Below is a new version of the ApiEventManager which should be solid, elegant, easy-to-understand and low-code.

Notes

  • Revert the Screen (1) and Tray (3) events to the original implementation
    It's not worth to introduce common handling for those 4 cases
  • What's next to do would be to
    • Remove the converter parameter, converters are no longer needed
    • Remove the callback parameter (not needed)
    • Remove all the private event variables that went into the callback parameter
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using ElectronNET.API;

namespace ElectronNET.Common;

internal static class ApiEventManager
{
    private static readonly object LockObj = new object();
    private static readonly ConcurrentDictionary<string, EventContainer> EventContainers = new();

    internal static void AddEvent(string eventName, int? id, Action callback, Action value)
    {
        var eventKey = EventKey(eventName, id);

        lock (LockObj)
        {
            var container = EventContainers.GetOrAdd(eventKey, _ =>
            {
                var container = new EventContainer();
                BridgeConnector.Socket.On(eventKey, container.OnEventAction);
                BridgeConnector.Socket.Emit($"register-{eventName}", id);
                return container;
            });

            container.Register(value);
        }
    }

    internal static void RemoveEvent(string eventName, int? id, Action callback, Action value)
    {
        var eventKey = EventKey(eventName, id);

        lock (LockObj)
        {
            if (EventContainers.TryGetValue(eventKey, out var container) && !container.Unregister(value))
            {
                BridgeConnector.Socket.Off(eventKey);
                EventContainers.TryRemove(eventKey, out _);
            }
        }
    }


    internal static void AddEvent<T>(string eventName, int? id, Action<T> callback, Action<T> value, Func<object, T> converter = null)
    {
        var eventKey = EventKey(eventName, id);

        lock (LockObj)
        {
            var container = EventContainers.GetOrAdd(eventKey, _ =>
            {
                var container = new EventContainer();
                BridgeConnector.Socket.On<T>(eventKey, container.OnEventActionT);
                BridgeConnector.Socket.Emit($"register-{eventName}", id);
                return container;
            });

            container.Register(value);
        }
    }

    internal static void RemoveEvent<T>(string eventName, int? id, Action<T> callback, Action<T> value)
    {
        var eventKey = EventKey(eventName, id);

        lock (LockObj)
        {
            if (EventContainers.TryGetValue(eventKey, out var container) && !container.Unregister(value))
            {
                BridgeConnector.Socket.Off(eventKey);
                EventContainers.TryRemove(eventKey, out _);
            }
        }
    }

    private static string EventKey(string eventName, int? id)
    {
        return string.Format(CultureInfo.InvariantCulture, "{0}{1:D}", eventName, id);
    }

    [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")]
    private class EventContainer
    {
        private Action eventAction;
        private Delegate eventActionT;

        private Action<T> GetEventActionT<T>()
        {
            return (Action<T>)this.eventActionT;
        }

        private void SetEventActionT<T>(Action<T> actionT)
        {
            this.eventActionT = actionT;
        }

        public void OnEventAction() => this.eventAction?.Invoke();

        public void OnEventActionT<T>(T p) => this.GetEventActionT<T>()?.Invoke(p);

        public void Register(Action receiver)
        {
            this.eventAction += receiver;
        }

        public void Register<T>(Action<T> receiver)
        {
            var actionT = this.GetEventActionT<T>();
            actionT += receiver;
            this.SetEventActionT(actionT);
        }

        public bool Unregister(Action receiver)
        {
            this.eventAction -= receiver;
            return this.eventAction != null;
        }

        public bool Unregister<T>(Action<T> receiver)
        {
            var actionT = this.GetEventActionT<T>();
            actionT -= receiver;
            this.SetEventActionT(actionT);

            return actionT != null;
        }
    }
}
@softworkz commented on GitHub (Nov 9, 2025): @agracio - This was much harder than I had thought and I had to try it out myself to be sure. Below is a new version of the ApiEventManager which should be solid, elegant, easy-to-understand and low-code. ### Notes - Revert the Screen (1) and Tray (3) events to the original implementation It's not worth to introduce common handling for those 4 cases - What's next to do would be to - Remove the converter parameter, converters are no longer needed - Remove the callback parameter (not needed) - Remove all the private event variables that went into the callback parameter ```csharp using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Globalization; using ElectronNET.API; namespace ElectronNET.Common; internal static class ApiEventManager { private static readonly object LockObj = new object(); private static readonly ConcurrentDictionary<string, EventContainer> EventContainers = new(); internal static void AddEvent(string eventName, int? id, Action callback, Action value) { var eventKey = EventKey(eventName, id); lock (LockObj) { var container = EventContainers.GetOrAdd(eventKey, _ => { var container = new EventContainer(); BridgeConnector.Socket.On(eventKey, container.OnEventAction); BridgeConnector.Socket.Emit($"register-{eventName}", id); return container; }); container.Register(value); } } internal static void RemoveEvent(string eventName, int? id, Action callback, Action value) { var eventKey = EventKey(eventName, id); lock (LockObj) { if (EventContainers.TryGetValue(eventKey, out var container) && !container.Unregister(value)) { BridgeConnector.Socket.Off(eventKey); EventContainers.TryRemove(eventKey, out _); } } } internal static void AddEvent<T>(string eventName, int? id, Action<T> callback, Action<T> value, Func<object, T> converter = null) { var eventKey = EventKey(eventName, id); lock (LockObj) { var container = EventContainers.GetOrAdd(eventKey, _ => { var container = new EventContainer(); BridgeConnector.Socket.On<T>(eventKey, container.OnEventActionT); BridgeConnector.Socket.Emit($"register-{eventName}", id); return container; }); container.Register(value); } } internal static void RemoveEvent<T>(string eventName, int? id, Action<T> callback, Action<T> value) { var eventKey = EventKey(eventName, id); lock (LockObj) { if (EventContainers.TryGetValue(eventKey, out var container) && !container.Unregister(value)) { BridgeConnector.Socket.Off(eventKey); EventContainers.TryRemove(eventKey, out _); } } } private static string EventKey(string eventName, int? id) { return string.Format(CultureInfo.InvariantCulture, "{0}{1:D}", eventName, id); } [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] private class EventContainer { private Action eventAction; private Delegate eventActionT; private Action<T> GetEventActionT<T>() { return (Action<T>)this.eventActionT; } private void SetEventActionT<T>(Action<T> actionT) { this.eventActionT = actionT; } public void OnEventAction() => this.eventAction?.Invoke(); public void OnEventActionT<T>(T p) => this.GetEventActionT<T>()?.Invoke(p); public void Register(Action receiver) { this.eventAction += receiver; } public void Register<T>(Action<T> receiver) { var actionT = this.GetEventActionT<T>(); actionT += receiver; this.SetEventActionT(actionT); } public bool Unregister(Action receiver) { this.eventAction -= receiver; return this.eventAction != null; } public bool Unregister<T>(Action<T> receiver) { var actionT = this.GetEventActionT<T>(); actionT -= receiver; this.SetEventActionT(actionT); return actionT != null; } } } ```
Author
Owner

@softworkz commented on GitHub (Nov 9, 2025):

PS: I created this new issue because the PR isn't really a good place for discussing this. 😉

@softworkz commented on GitHub (Nov 9, 2025): PS: I created this new issue because the PR isn't really a good place for discussing this. 😉
Author
Owner

@agracio commented on GitHub (Nov 9, 2025):

I have done the following: https://github.com/agracio/Electron.NET/blob/develop/src/ElectronNET.API/API/ApiBase.cs
Scroll to line 126.

The only thing that is missing is locks in my opinion.

Remove the converter parameter, converters are no longer needed

I will be happy to do that but are you sure Socket can handle correct serialization/deserialization? The only reason I have converters is because that is how it is used in original code.

Example

public event Action<UpdateInfo> OnUpdateAvailable
{
    add => ApiEventManager.AddEvent("autoUpdater-update-available", GetHashCode(), _updateAvailable, value, (args) => JObject.Parse(args.ToString()).ToObject<UpdateInfo>());
    remove => ApiEventManager.RemoveEvent("autoUpdater-update-available", GetHashCode(), _updateAvailable, value);
}
public event Action<OnDidFailLoadInfo> OnDidFailLoad
{
    add => ApiEventManager.AddEvent("webContents-didFailLoad", Id, _didFailLoad, value, (args) => ((JObject)args).ToObject<OnDidFailLoadInfo>());
    remove => ApiEventManager.RemoveEvent("webContents-didFailLoad", Id, _didFailLoad, value);
}
@agracio commented on GitHub (Nov 9, 2025): I have done the following: https://github.com/agracio/Electron.NET/blob/develop/src/ElectronNET.API/API/ApiBase.cs Scroll to line 126. The only thing that is missing is locks in my opinion. > Remove the converter parameter, converters are no longer needed I will be happy to do that but are you sure Socket can handle correct serialization/deserialization? The only reason I have converters is because that is how it is used in original code. Example ```cs public event Action<UpdateInfo> OnUpdateAvailable { add => ApiEventManager.AddEvent("autoUpdater-update-available", GetHashCode(), _updateAvailable, value, (args) => JObject.Parse(args.ToString()).ToObject<UpdateInfo>()); remove => ApiEventManager.RemoveEvent("autoUpdater-update-available", GetHashCode(), _updateAvailable, value); } ``` ```cs public event Action<OnDidFailLoadInfo> OnDidFailLoad { add => ApiEventManager.AddEvent("webContents-didFailLoad", Id, _didFailLoad, value, (args) => ((JObject)args).ToObject<OnDidFailLoadInfo>()); remove => ApiEventManager.RemoveEvent("webContents-didFailLoad", Id, _didFailLoad, value); } ```
Author
Owner

@softworkz commented on GitHub (Nov 9, 2025):

I have done the following: agracio/Electron.NET@develop/src/ElectronNET.API/API/ApiBase.cs
Scroll to line 126.

This is great, I would have moved it there as well and using CallerMemberName is cool on top.

The only thing that is missing is locks in my opinion.

And that is the problem. I've been sitting on this for 2.5h - almost since you had asked about locks.
Since you have already gone in the same direction, you just need to copy the EventContainer class into ApiBase and the 4 method bodies for Add/RemoveEvent.

You can try to figure out why I'm saying it's better, or I can explain tomorrow (I need a break now...)

Remove the converter parameter, converters are no longer needed

I will be happy to do that but are you sure Socket can handle correct serialization/deserialization? The only reason I have converters is because that is how it is used in original code.

The original code used the old SocketIO which couldn't properly handle it. For ElectronNET.Core, I had updated all libraries (but not the code).
I had tried without converters and all tests have still succeeded. I mean, you can also start by just ignoring the converters and see how it goes on your side.

Do you have something like ReSharper? Not that you start doing those removals by hand... 😆 ...it's a matter of 5 seconds.

@softworkz commented on GitHub (Nov 9, 2025): > I have done the following: [agracio/Electron.NET@`develop`/src/ElectronNET.API/API/ApiBase.cs](https://github.com/agracio/Electron.NET/blob/develop/src/ElectronNET.API/API/ApiBase.cs?rgh-link-date=2025-11-09T17%3A33%3A32.000Z) > Scroll to line 126. This is great, I would have moved it there as well and using CallerMemberName is cool on top. > The only thing that is missing is locks in my opinion. And that is the problem. I've been sitting on this for 2.5h - almost since you had asked about locks. Since you have already gone in the same direction, you just need to copy the EventContainer class into ApiBase and the 4 method bodies for Add/RemoveEvent. You can try to figure out why I'm saying it's better, or I can explain tomorrow (I need a break now...) > > Remove the converter parameter, converters are no longer needed > > I will be happy to do that but are you sure Socket can handle correct serialization/deserialization? The only reason I have converters is because that is how it is used in original code. The original code used the old SocketIO which couldn't properly handle it. For ElectronNET.Core, I had updated all libraries (but not the code). I had tried without converters and all tests have still succeeded. I mean, you can also start by just ignoring the converters and see how it goes on your side. Do you have something like ReSharper? Not that you start doing those removals by hand... 😆 ...it's a matter of 5 seconds.
Author
Owner

@softworkz commented on GitHub (Nov 9, 2025):

I want to stress that I like the changes you made. If you hadn't made me think about locking, I would have said that it's very good..

@softworkz commented on GitHub (Nov 9, 2025): I want to stress that I like the changes you made. If you hadn't made me think about locking, I would have said that it's very good..
Author
Owner

@agracio commented on GitHub (Nov 9, 2025):

Do you have something like ReSharper?

Of course 😄

I mean it is Jetbrains Rider it could not be any other way.

@agracio commented on GitHub (Nov 9, 2025): > Do you have something like ReSharper? Of course 😄 I mean it is Jetbrains Rider it could not be any other way.
Author
Owner

@FlorianRappl commented on GitHub (Nov 9, 2025):

Honestly, I think I can solve that problem by shifting it.

Let me explain: The main problem with the .NET code is that multi threading has always been a bit of pain. That's why many modern languages try to circumvent the issue by essentially only allowing code / variables to be touched by the owning thread (fiber, coroutine, whatever you want to call it - for sake of argument lets treat them as equal here). JavaScript for instance is single-threaded, with Node.js using a Reactor pattern to let all native code flow into that single thread.

That all being written - what if we just let the IPC counter part handle all that? Right now what we do:

  • Event is added:
    • In .NET: Let's see if the event has been added, if not, let's register it
    • Add the handler
    • In Node.js: Only add event command is received - then always reply back to that one source
  • Event is removed:
    • In .NET: Let's see if the event has only handlers left; if not - let's unregister it
    • Remove the handler
    • In Node.js: Remove event command is received - removal of Electron event handler

What I propose is to leave that in Node.js:

  • Event is added:
    • In .NET: Let's send the command to register an event with the ID of the handler
    • In Node.js: Have a map with event names to handler IDs - if the given event name is not present let's start listening. The listener opens the entry in the map and sends the args to all registered handlers.
  • Event is removed:
    • In .NET: Let's send the command to unregister an event with the ID of the handler
    • In Node.js: Have a map with event names to handler IDs - if the given event name is present let's remove the handler ID from the entry. The listener (and the entry) is fully removed if no handler IDs remain.

Result? Node.js handles the concurrent part, which it will do very well. In .NET we only have a simple logic to reason with.

Any thoughts on that one @softworkz @agracio ?

Remark: Improving the main.js / Node.js process can also be done to avoid having Electron processes floating around. Once the parent process ID is removed / no longer active the process could be shut down. This could maybe be also achieved with a periodic keep alive from the WebSocket channel. If the keep alive is missing, we'll close the process.

@FlorianRappl commented on GitHub (Nov 9, 2025): Honestly, I think I can solve that problem by shifting it. Let me explain: The main problem with the .NET code is that multi threading has always been a bit of pain. That's why many modern languages try to circumvent the issue by essentially only allowing code / variables to be touched by the owning thread (fiber, coroutine, whatever you want to call it - for sake of argument lets treat them as equal here). JavaScript for instance is single-threaded, with Node.js using a Reactor pattern to let all native code flow into that single thread. That all being written - what if we just let the IPC counter part handle all that? Right now what we do: - Event is added: - In .NET: Let's see if the event has been added, if not, let's register it - Add the handler - In Node.js: Only add event command is received - then always reply back to that one source - Event is removed: - In .NET: Let's see if the event has only handlers left; if not - let's unregister it - Remove the handler - In Node.js: Remove event command is received - removal of Electron event handler What I propose is to leave that in Node.js: - Event is added: - In .NET: Let's send the command to register an event with the ID of the handler - In Node.js: Have a map with event names to handler IDs - if the given event name is not present let's start listening. The listener opens the entry in the map and sends the args to all registered handlers. - Event is removed: - In .NET: Let's send the command to unregister an event with the ID of the handler - In Node.js: Have a map with event names to handler IDs - if the given event name is present let's remove the handler ID from the entry. The listener (and the entry) is fully removed if no handler IDs remain. Result? Node.js handles the concurrent part, which it will do very well. In .NET we only have a simple logic to reason with. Any thoughts on that one @softworkz @agracio ? **Remark**: Improving the `main.js` / Node.js process can also be done to avoid having Electron processes floating around. Once the parent process ID is removed / no longer active the process could be shut down. This could maybe be also achieved with a periodic keep alive from the WebSocket channel. If the keep alive is missing, we'll close the process.
Author
Owner

@agracio commented on GitHub (Nov 9, 2025):

Remark: Improving the main.js / Node.js process can also be done to avoid having Electron processes floating around. Once the parent process ID is removed / no longer active the process could be shut down. This could maybe be also achieved with a periodic keep alive from the WebSocket channel. If the keep alive is missing, we'll close the process.

I was thinking that it can be done the same way but keep wondering if there is a more elegant solution. To be honest probably not.
In a few hours of writing code and running tests had about 15 hanging electron.exe processes.

As for improving IPC - no thoughts about it yet. Refactoring code tends to create a very narrow field of view for me until it is finished. In other words right now I don't see the forest for the trees.

Will try to get a better overall view some time later.

@agracio commented on GitHub (Nov 9, 2025): > Remark: Improving the main.js / Node.js process can also be done to avoid having Electron processes floating around. Once the parent process ID is removed / no longer active the process could be shut down. This could maybe be also achieved with a periodic keep alive from the WebSocket channel. If the keep alive is missing, we'll close the process. I was thinking that it can be done the same way but keep wondering if there is a more elegant solution. To be honest probably not. In a few hours of writing code and running tests had about 15 hanging electron.exe processes. As for improving IPC - no thoughts about it yet. Refactoring code tends to create a very narrow field of view for me until it is finished. In other words right now I don't see the forest for the trees. Will try to get a better overall view some time later.
Author
Owner

@softworkz commented on GitHub (Nov 9, 2025):

You probably both missed my post https://github.com/ElectronNET/Electron.NET/pull/908#issuecomment-3506758001 - but admittedly, it's not a good idea to spin long discussions in PRs. So here's sure better. Repost:

I can only guess that thread syntonisation and SocketIO stability are the main issues.

Well, those are the collaterals, but the source of all evil is something quite different:

It's the use of SocketIO named events, and trying to squeeze the behavior that we would actually need into the limitations and constraints of SocketIO events.
That is the major design - let's say 'flaw' - that is leading to all the problems that we (and the fork) have to fight against.

The current implementation tries to establish short-lived SocketIO event channels "per invocation":

  • This (unnecessarily) introduces the limitation that a remote invocation under a given (Socket-)event name needs to be serialized (which is what my PropertyGet and the fork code is attempting to do):
    • There can be only a single ongoing invocation at a time, for example, when the client code wants to retrieve the size of multiple windows at the same time, the invocation for the 2nd window needs to wait until the invocation for the 1st window has returned and the invocation for the 3rd window needs to wait for the 1st and 2nd invocation to be completed, etc.
    • When one of the invocations doesn't return (for some reason), the others would be blocked forever (unless there's a timeout, but then the other invocations would still be blocked until the timeout is reached)
    • When there are many invocations for the same property on the same window, both approaches (fork and mine) are returning the task for the current outstanding invocation - so they'll get the same result as all previous pending implementations.
      That's only a band-aid, but not really a good solution, because when the subsequent invocations could be made in parallel, they might not receive the same return value than you get from the still pending invocation. This can be important when you need to synchronize window sizes and/or positions.
      In my case for example, I had to resort to using the native X-Window (x11Lib) API via P/Invoke in order to achieve a fast and reliable synchroniztation of windows on Linux (the were other reasons for this as well, though)
  • The fact that On() and Off() are not even synchronized at the side of SocketIO shows that SocketIO was never even meant to be used that way

The Ideal Way

I have a bit of a problem talking about this, because I don't want to make any promises that I won't be able to fulfill, but as a matter of fact, I have done an implementation for bidirectional JS(browser) <=> CSharp projection which brings JS objects to C# and C# objects to JS. It supports method/function calling (async + sync from JS => C#), property retrieval and event registration (in both directions). It works cross-browser and "cross-ui-framework" (UWP/Xbox, WinUI w. WebView2, Electron, Android, iOS, Tizen/untested), battle-tested (approaching 1M installations) and high-performant (it can sync the movement of a CSS transition to a hWND native window on Windows).

Communication is message-based and all goes through a single channel. That would mean - in regards to SocketIO - there would be a single On('message') at app start and a single Off('message') on shutdown. Messages have a serial number, which means that - no matter how many invocations are active in parallel - each invocation will always get its own response, without any collisions or race conditions.

On the JS side, all invocations are be made directly using the JS function prototypes,, there's no need to create any proxy classes like al the TS/JS code under ElectronNET.Host/api - only some edge cases would need special/explicit handling.
At the C# side, the JS objects are represented by interfaces (which are autogenerated). At runtime, objects are materialized at runtime via System.Reflection.DispatchProxy.

So - that's a recipe that works and that I would use when building ElectronNET from scratch.
I don't think that I'll have time to adapt this for ElectronNET - but the messaging part alone could be implemented with a lot less effort. The TypeScript code rewriting could probably be tasked to an AI agent once the required architecture change is implemented.

@softworkz commented on GitHub (Nov 9, 2025): You probably both missed my post https://github.com/ElectronNET/Electron.NET/pull/908#issuecomment-3506758001 - but admittedly, it's not a good idea to spin long discussions in PRs. So here's sure better. Repost: > I can only guess that thread syntonisation and SocketIO stability are the main issues. Well, those are the collaterals, but the source of all evil is something quite different: It's the use of SocketIO named events, and trying to squeeze the behavior that we would actually need into the limitations and constraints of SocketIO events. That is the major design - let's say 'flaw' - that is leading to all the problems that we (and the fork) have to fight against. The current implementation tries to establish short-lived SocketIO event channels "per invocation": - This (unnecessarily) introduces the limitation that a remote invocation under a given (Socket-)event name needs to be serialized (which is what my PropertyGet and the fork code is attempting to do): - There can be only a single ongoing invocation at a time, for example, when the client code wants to retrieve the size of multiple windows at the same time, the invocation for the 2nd window needs to wait until the invocation for the 1st window has returned and the invocation for the 3rd window needs to wait for the 1st and 2nd invocation to be completed, etc. - When one of the invocations doesn't return (for some reason), the others would be blocked forever (unless there's a timeout, but then the other invocations would still be blocked until the timeout is reached) - When there are many invocations for the same property on the same window, both approaches (fork and mine) are returning the task for the current outstanding invocation - so they'll get the same result as all previous pending implementations. That's only a band-aid, but not really a good solution, because when the subsequent invocations could be made in parallel, they might not receive the same return value than you get from the still pending invocation. This can be important when you need to synchronize window sizes and/or positions. In my case for example, I had to resort to using the native X-Window (x11Lib) API via P/Invoke in order to achieve a fast and reliable synchroniztation of windows on Linux (the were other reasons for this as well, though) - The fact that On() and Off() are not even synchronized at the side of SocketIO shows that SocketIO was never even meant to be used that way ### The Ideal Way I have a bit of a problem talking about this, because I don't want to make any promises that I won't be able to fulfill, but as a matter of fact, I have done an implementation for bidirectional JS(browser) <=> CSharp projection which brings JS objects to C# and C# objects to JS. It supports method/function calling (async + sync from JS => C#), property retrieval and event registration (in both directions). It works cross-browser and "cross-ui-framework" (UWP/Xbox, WinUI w. WebView2, Electron, Android, iOS, Tizen/untested), battle-tested (approaching 1M installations) and high-performant (it can sync the movement of a CSS transition to a hWND native window on Windows). Communication is message-based and all goes through a single channel. That would mean - in regards to SocketIO - there would be a single `On('message')` at app start and a single `Off('message')` on shutdown. Messages have a serial number, which means that - no matter how many invocations are active in parallel - each invocation will always get its own response, without any collisions or race conditions. On the JS side, all invocations are be made directly using the JS function prototypes,, there's no need to create any proxy classes like al the TS/JS code under `ElectronNET.Host/api` - only some edge cases would need special/explicit handling. At the C# side, the JS objects are represented by interfaces (which are autogenerated). At runtime, objects are materialized at runtime via System.Reflection.DispatchProxy. So - that's a recipe that works and that I would use when building ElectronNET from scratch. I don't think that I'll have time to adapt this for ElectronNET - but the messaging part alone could be implemented with a lot less effort. The TypeScript code rewriting could probably be tasked to an AI agent once the required architecture change is implemented.
Author
Owner

@agracio commented on GitHub (Nov 9, 2025):

The original code used the old SocketIO which couldn't properly handle it. For ElectronNET.Core, I had updated all libraries (but not the code).
I had tried without converters and all tests have still succeeded. I mean, you can also start by just ignoring the converters and see how it goes on your side.

Would it be able to handle enums?

var themeSourceValue = (ThemeSourceMode)Enum.Parse(typeof(ThemeSourceMode), (string)themeSource, true);

taskCompletionSource.SetResult(themeSourceValue);

Asking to make sure I can make changes.

EDIT: I will be adding tests alongside the changes so should be able to check soon.

@agracio commented on GitHub (Nov 9, 2025): >The original code used the old SocketIO which couldn't properly handle it. For ElectronNET.Core, I had updated all libraries (but not the code). I had tried without converters and all tests have still succeeded. I mean, you can also start by just ignoring the converters and see how it goes on your side. Would it be able to handle enums? ```cs var themeSourceValue = (ThemeSourceMode)Enum.Parse(typeof(ThemeSourceMode), (string)themeSource, true); taskCompletionSource.SetResult(themeSourceValue); ``` Asking to make sure I can make changes. EDIT: I will be adding tests alongside the changes so should be able to check soon.
Author
Owner

@softworkz commented on GitHub (Nov 9, 2025):

Would it be able to handle enums?
var themeSourceValue = (ThemeSourceMode)Enum.Parse(typeof(ThemeSourceMode), (string)themeSource, true);
taskCompletionSource.SetResult(themeSourceValue);

Like this? No (we have differences in casing and sometimes even more different names where the mapping is done via certain attributes on enum members. Enum.Parse() (better use the generic version) can do case-insensitive comparisons, but that's it then. Only a properly configured json serializer can do this.

But this should work:

Socket.On(....

@softworkz commented on GitHub (Nov 9, 2025): > Would it be able to handle enums? > var themeSourceValue = (ThemeSourceMode)Enum.Parse(typeof(ThemeSourceMode), (string)themeSource, true); > taskCompletionSource.SetResult(themeSourceValue); Like this? No (we have differences in casing and sometimes even more different names where the mapping is done via certain attributes on enum members. `Enum.Parse()` (better use the generic version) can do case-insensitive comparisons, but that's it then. Only a properly configured json serializer can do this. But this should work: Socket.On<ThemeSourceMode>(....
Author
Owner

@agracio commented on GitHub (Nov 9, 2025):

public Task<ThemeSourceMode> GetThemeSourceAsync() => GetPropertyAsync<ThemeSourceMode>();

works perfectly and is included in test 😉

@agracio commented on GitHub (Nov 9, 2025): ```cs public Task<ThemeSourceMode> GetThemeSourceAsync() => GetPropertyAsync<ThemeSourceMode>(); ``` works perfectly and is included in test 😉
Author
Owner

@agracio commented on GitHub (Nov 10, 2025):

ApiEventManager rework is taking longer than expected as I am also refactoring tasks to use new ApiBase.
There are some obvious mistakes that I have to correct as well as just plain renaming of current event names. And of course having to add tests to any new tasks.

@agracio commented on GitHub (Nov 10, 2025): ApiEventManager rework is taking longer than expected as I am also refactoring tasks to use new `ApiBase`. There are some obvious mistakes that I have to correct as well as just plain renaming of current event names. And of course having to add tests to any new tasks.
Author
Owner

@agracio commented on GitHub (Nov 10, 2025):

Well apparently there was a major PR merged into develop while I was working and now pretty much everything is conflicting so no ETA for now. Was just about to create PR.
Will go bang my head against some walls and then maybe much later today or tomorrow will have something.

@agracio commented on GitHub (Nov 10, 2025): Well apparently there was a major PR merged into `develop` while I was working and now pretty much everything is conflicting so no ETA for now. Was just about to create PR. Will go bang my head against some walls and then maybe much later today or tomorrow will have something.
Author
Owner

@FlorianRappl commented on GitHub (Nov 10, 2025):

There is no need for you doing all the work. The PR has been announced for a while - sorry that you are going through that.

If you need help just make the PR, open the branch for changes and the conflicts can be resolved collaboratively.

Other question is if we should merge #920 already. It will for sure cause more conflicts, but the question is if its more efficient to do that in one sweep or use two takes (I tend to think that having conflicts once is more efficient than dealing with them twice).

@FlorianRappl commented on GitHub (Nov 10, 2025): There is no need for you doing all the work. The PR has been announced for a while - sorry that you are going through that. If you need help just make the PR, open the branch for changes and the conflicts can be resolved collaboratively. Other question is if we should merge #920 already. It will for sure cause more conflicts, but the question is if its more efficient to do that in one sweep or use two takes (I tend to think that having conflicts once is more efficient than dealing with them twice).
Author
Owner

@agracio commented on GitHub (Nov 10, 2025):

@softworkz should take a close look at https://github.com/ElectronNET/Electron.NET/pull/920 since it makes alot of changes to tasks. I will pause any work until all PRs are merged there is no point to do any changes right now.

I have modified quiet a few files to use ApiBase GetProperty instead of calling BridgeConnector so that is another big source of conflicts.

@agracio commented on GitHub (Nov 10, 2025): @softworkz should take a close look at https://github.com/ElectronNET/Electron.NET/pull/920 since it makes alot of changes to tasks. I will pause any work until all PRs are merged there is no point to do any changes right now. I have modified quiet a few files to use ApiBase GetProperty instead of calling BridgeConnector so that is another big source of conflicts.
Author
Owner

@softworkz commented on GitHub (Nov 10, 2025):

@softworkz should take a close look at #920 since it makes alot of changes to tasks. I will pause any work until all PRs are merged there is no point to do any changes right now.

#920 is fine. You do not need to pause. I have rebased your branch on top of #920 and it had (simple) conflicts in two files only (Screen.cs and another). Easy to resolve. The result is here: https://github.com/softworkz/ElectronNET/tree/agracio_event_mgr_rebased

When you get conflicts on rebasing (after 920 merged), you can just take the whole file content from that branch to resolve.

@softworkz commented on GitHub (Nov 10, 2025): > [@softworkz](https://github.com/softworkz) should take a close look at [#920](https://github.com/ElectronNET/Electron.NET/pull/920) since it makes alot of changes to tasks. I will pause any work until all PRs are merged there is no point to do any changes right now. #920 is fine. You do not need to pause. I have rebased your branch on top of #920 and it had (simple) conflicts in two files only (Screen.cs and another). Easy to resolve. The result is here: https://github.com/softworkz/ElectronNET/tree/agracio_event_mgr_rebased When you get conflicts on rebasing (after 920 merged), you can just take the whole file content from that branch to resolve.
Author
Owner

@softworkz commented on GitHub (Nov 10, 2025):

If you need help just make the PR, open the branch for changes and the conflicts can be resolved collaboratively.

Agree, I'll also happily help reolving - these are all quite trivial conflicts. It's just a matter of tooling and having some routine in working with Git in those cases.

@softworkz commented on GitHub (Nov 10, 2025): > If you need help just make the PR, open the branch for changes and the conflicts can be resolved collaboratively. Agree, I'll also happily help reolving - these are all quite trivial conflicts. It's just a matter of tooling and having some routine in working with Git in those cases.
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

When using BridgeConnector.Socket.Emit is there a difference between two calls in the code below, will an On method treat them as different calls? Unfortunately I am not familiar with sockets that well.

var messageName = "message";
var id = 0;

BridgeConnector.Socket.Emit(messageName, id, null);
// and
BridgeConnector.Socket.Emit(messageName, id);
@agracio commented on GitHub (Nov 11, 2025): When using `BridgeConnector.Socket.Emit` is there a difference between two calls in the code below, will an `On` method treat them as different calls? Unfortunately I am not familiar with sockets that well. ```cs var messageName = "message"; var id = 0; BridgeConnector.Socket.Emit(messageName, id, null); // and BridgeConnector.Socket.Emit(messageName, id); ```
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

It has nothing to do with Sockets. The parameters are just serialized to an array (JSON).

So, for the latter, the remote side will see [ 0 ] and for the former [ 0, null ]. So whether it matters or not, depends on how it's handled.

@softworkz commented on GitHub (Nov 11, 2025): It has nothing to do with Sockets. The parameters are just serialized to an array (JSON). So, for the latter, the remote side will see `[ 0 ]` and for the former `[ 0, null ]`. So whether it matters or not, depends on how it's handled.
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

Thanks for quick reply, just tested in Electron.NET (we have tests now!!). The answer is that it is treated as same message.
This means that I can use this code in ApiBase and add more nullable args if needed without having to jump through multiple ifs.

if (apiBase.Id >= 0)
{
    BridgeConnector.Socket.Emit(messageName, apiBase.Id, arg);
}
else
{
    BridgeConnector.Socket.Emit(messageName, arg);
}

This also mean that you don't need to have CallMethod0, CallMethod1, CallMethod2, CallMethod3

@agracio commented on GitHub (Nov 11, 2025): Thanks for quick reply, just tested in Electron.NET (we have tests now!!). The answer is that it is treated as same message. This means that I can use this code in `ApiBase` and add more nullable args if needed without having to jump through multiple ifs. ```cs if (apiBase.Id >= 0) { BridgeConnector.Socket.Emit(messageName, apiBase.Id, arg); } else { BridgeConnector.Socket.Emit(messageName, arg); } ``` This also mean that you don't need to have `CallMethod0`, `CallMethod1`, `CallMethod2`, `CallMethod3`
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

This also mean that you don't need to have CallMethod0, CallMethod1, CallMethod2, CallMethod3

Oh, no - that it doesn't mean...

@softworkz commented on GitHub (Nov 11, 2025): > This also mean that you don't need to have `CallMethod0`, `CallMethod1`, `CallMethod2`, `CallMethod3` Oh, no - that it doesn't mean...
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

If the above code works with a nullable argument then this type of method should also produce the same results

protected void CallMethod(object val1 = null, object val2 = null, object val3 = null, [CallerMemberName] string callerName = null)
{
    var messageName = this.methodMessageNames.GetOrAdd(callerName, s => this.objectName + s);
    if (this.Id >= 0)
    {
        BridgeConnector.Socket.Emit(messageName, this.Id, val1, val2, val3);
    }
    else
    {
        BridgeConnector.Socket.Emit(messageName, val1, val2, val3);
    }
}

So far tests agree.

@agracio commented on GitHub (Nov 11, 2025): If the above code works with a nullable argument then this type of method should also produce the same results ```cs protected void CallMethod(object val1 = null, object val2 = null, object val3 = null, [CallerMemberName] string callerName = null) { var messageName = this.methodMessageNames.GetOrAdd(callerName, s => this.objectName + s); if (this.Id >= 0) { BridgeConnector.Socket.Emit(messageName, this.Id, val1, val2, val3); } else { BridgeConnector.Socket.Emit(messageName, val1, val2, val3); } } ``` So far tests agree.
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

I said:

whether it matters or not, depends on how it's handled.

See this example:

Image
@softworkz commented on GitHub (Nov 11, 2025): I said: > whether it matters or not, depends on how it's handled. See this example: <img width="1196" height="433" alt="Image" src="https://github.com/user-attachments/assets/8aa2f9f9-f46e-4d53-bed2-4340dc0216a7" />
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

Very good example, nullable method will not work if input to a same event can be optional. But hopefully a good 85% of cases can be simplified.

There are also tasks that use cancellation token and are not currently handled by ApiBase.

@agracio commented on GitHub (Nov 11, 2025): Very good example, nullable method will not work if input to a same event can be optional. But hopefully a good 85% of cases can be simplified. There are also tasks that use cancellation token and are not currently handled by ApiBase.
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

Btw the task you highlighted is on the tasks that use new Guid in addition to Id, there are other tasks that use only new Guid without Id but tbh since it is always a new Guid I feel that Id should not even be there. Do you have any clues as to why that is.

public Task ClearAuthCacheAsync(RemovePassword options)
{
    var taskCompletionSource = new TaskCompletionSource<object>();
    string guid = Guid.NewGuid().ToString();
    BridgeConnector.Socket.On("webContents-session-clearAuthCache-completed" + guid, () =>
    {
        BridgeConnector.Socket.Off("webContents-session-clearAuthCache-completed" + guid);
        taskCompletionSource.SetResult(null);
    });
    BridgeConnector.Socket.Emit("webContents-session-clearAuthCache", Id, options, guid);
    return taskCompletionSource.Task;
}

public Task ClearAuthCacheAsync()
{
    var taskCompletionSource = new TaskCompletionSource<object>();
    string guid = Guid.NewGuid().ToString();
    BridgeConnector.Socket.On("webContents-session-clearAuthCache-completed" + guid, () =>
    {
        BridgeConnector.Socket.Off("webContents-session-clearAuthCache-completed" + guid);
        taskCompletionSource.SetResult(null);
    });
    BridgeConnector.Socket.Emit("webContents-session-clearAuthCache", Id, guid);
    return taskCompletionSource.Task;
}
public Task<string> DownloadUpdateAsync()
{
    var taskCompletionSource = new TaskCompletionSource<string>();
    string guid = Guid.NewGuid().ToString();
    BridgeConnector.Socket.On<string>("autoUpdaterDownloadUpdateComplete" + guid, (downloadedPath) =>
    {
        BridgeConnector.Socket.Off("autoUpdaterDownloadUpdateComplete" + guid);
        taskCompletionSource.SetResult(downloadedPath);
    });
    BridgeConnector.Socket.Emit("autoUpdaterDownloadUpdate", guid);
    return taskCompletionSource.Task;
}
@agracio commented on GitHub (Nov 11, 2025): Btw the task you highlighted is on the tasks that use new Guid in addition to Id, there are other tasks that use only new Guid without Id but tbh since it is always a new Guid I feel that Id should not even be there. Do you have any clues as to why that is. ```cs public Task ClearAuthCacheAsync(RemovePassword options) { var taskCompletionSource = new TaskCompletionSource<object>(); string guid = Guid.NewGuid().ToString(); BridgeConnector.Socket.On("webContents-session-clearAuthCache-completed" + guid, () => { BridgeConnector.Socket.Off("webContents-session-clearAuthCache-completed" + guid); taskCompletionSource.SetResult(null); }); BridgeConnector.Socket.Emit("webContents-session-clearAuthCache", Id, options, guid); return taskCompletionSource.Task; } public Task ClearAuthCacheAsync() { var taskCompletionSource = new TaskCompletionSource<object>(); string guid = Guid.NewGuid().ToString(); BridgeConnector.Socket.On("webContents-session-clearAuthCache-completed" + guid, () => { BridgeConnector.Socket.Off("webContents-session-clearAuthCache-completed" + guid); taskCompletionSource.SetResult(null); }); BridgeConnector.Socket.Emit("webContents-session-clearAuthCache", Id, guid); return taskCompletionSource.Task; } ``` ```cs public Task<string> DownloadUpdateAsync() { var taskCompletionSource = new TaskCompletionSource<string>(); string guid = Guid.NewGuid().ToString(); BridgeConnector.Socket.On<string>("autoUpdaterDownloadUpdateComplete" + guid, (downloadedPath) => { BridgeConnector.Socket.Off("autoUpdaterDownloadUpdateComplete" + guid); taskCompletionSource.SetResult(downloadedPath); }); BridgeConnector.Socket.Emit("autoUpdaterDownloadUpdate", guid); return taskCompletionSource.Task; } ```
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

But hopefully a good 85% of cases can be simplified.

Please leave that as is. The four methods are just 50 lines of code and not a good subject for "simplification". Especially in this case: it would not be clear to other developers in the future why this or that is used and then they might think that they can "simplify" it even more by applying the same pattern to those cases where it matters - not knowing that it will break things.

Also - after thinking about it, I think it's generally odd to always provide 3 parameters regardless of how many params are actually required and expected. That's not an improvement.

@softworkz commented on GitHub (Nov 11, 2025): > But hopefully a good 85% of cases can be simplified. Please leave that as is. The four methods are just 50 lines of code and not a good subject for "simplification". Especially in this case: it would not be clear to other developers in the future why this or that is used and then they might think that they can "simplify" it even more by applying the same pattern to those cases where it matters - not knowing that it will break things. Also - after thinking about it, I think it's generally odd to always provide 3 parameters regardless of how many params are actually required and expected. That's not an improvement.
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

CallMethod is actually a small example, GetProperty is a bigger one and I have already added a nullable object to it but might need to add a second one, will handle it with ifs instead.

@agracio commented on GitHub (Nov 11, 2025): `CallMethod` is actually a small example, `GetProperty` is a bigger one and I have already added a nullable object to it but might need to add a second one, will handle it with ifs instead.
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

Btw the task you highlighted is on the tasks that use new Guid in addition to Id, there are other tasks that use only new Guid without Id but tbh since it is always a new Guid I feel that Id should not even be there. Do you have any clues as to why that is.

The Id is needed to retrieve the window at the electron side and the guid is used for the event name.

@softworkz commented on GitHub (Nov 11, 2025): > Btw the task you highlighted is on the tasks that use new Guid in addition to Id, there are other tasks that use only new Guid without Id but tbh since it is always a new Guid I feel that Id should not even be there. Do you have any clues as to why that is. The Id is needed to retrieve the window at the electron side and the guid is used for the event name.
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

GetProperty is a bigger one and I have already added a nullable object to it but might need to add a second one.

Why?

@softworkz commented on GitHub (Nov 11, 2025): > `GetProperty` is a bigger one and I have already added a nullable object to it but might need to add a second one. Why?
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

Example Screen.cs

public Task<Display> GetDisplayNearestPointAsync(Point point) => GetPropertyAsync<Display>(point);
public Task<Display> GetDisplayMatchingAsync(Rectangle rectangle) => GetPropertyAsync<Display>(rectangle);

ApiBase

protected Task<T> GetPropertyAsync<T>(object arg = null, [CallerMemberName] string callerName = null){}

 public PropertyGetter(ApiBase apiBase, string callerName, int timeoutMs, object arg = null){
//...

if (arg != null)
{
    _ = apiBase.Id >= 0 ? BridgeConnector.Socket.Emit(messageName, apiBase.Id, arg) :  BridgeConnector.Socket.Emit(messageName, arg)
}
else
{
    _ = apiBase.Id >= 0 ? BridgeConnector.Socket.Emit(messageName, apiBase.Id) :  BridgeConnector.Socket.Emit(messageName);
}
// ...
}
@agracio commented on GitHub (Nov 11, 2025): Example Screen.cs ```cs public Task<Display> GetDisplayNearestPointAsync(Point point) => GetPropertyAsync<Display>(point); public Task<Display> GetDisplayMatchingAsync(Rectangle rectangle) => GetPropertyAsync<Display>(rectangle); ``` ApiBase ```cs protected Task<T> GetPropertyAsync<T>(object arg = null, [CallerMemberName] string callerName = null){} public PropertyGetter(ApiBase apiBase, string callerName, int timeoutMs, object arg = null){ //... if (arg != null) { _ = apiBase.Id >= 0 ? BridgeConnector.Socket.Emit(messageName, apiBase.Id, arg) : BridgeConnector.Socket.Emit(messageName, arg) } else { _ = apiBase.Id >= 0 ? BridgeConnector.Socket.Emit(messageName, apiBase.Id) : BridgeConnector.Socket.Emit(messageName); } // ... } ```
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

Ah, I see. From a strict point of view, these are fuctions, not properties.. 😉

@softworkz commented on GitHub (Nov 11, 2025): Ah, I see. From a strict point of view, these are fuctions, not properties.. 😉
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

We might need to rename it later, but sure, why not. When an existing pattern can be applied, that's often better than creating many separate ones.

@softworkz commented on GitHub (Nov 11, 2025): We might need to rename it later, but sure, why not. When an existing pattern can be applied, that's often better than creating many separate ones.
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

I am not the one who named it GetProperty - you initially used it in properties as well as methods but later reverted property usage due to incompatible event naming. But I will fix those as well.

@agracio commented on GitHub (Nov 11, 2025): I am not the one who named it `GetProperty` - you initially used it in properties as well as methods but later reverted property usage due to incompatible event naming. But I will fix those as well.
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

I am not the one who named it GetProperty - you initially used it in properties as well as methods

I used it in methods? I thought I used CallMethodX for methods, no? But honestly I don't remember all cases, I'm doing so many things in parallel...

More importantly

We'll also need to make the dictionary static. Currently there's no locking when the same call is made on different objects of the same type. I just didn't come to a good idea (without additional dictionary key prefixing). But now I have - and it's a very simple change - you can do it since you're at it anyway:

Image
@softworkz commented on GitHub (Nov 11, 2025): > I am not the one who named it GetProperty - you initially used it in properties as well as methods I used it in methods? I thought I used CallMethodX for methods, no? But honestly I don't remember all cases, I'm doing so many things in parallel... #### More importantly We'll also need to make the dictionary static. Currently there's no locking when the same call is made on different objects of the same type. I just didn't come to a good idea (without additional dictionary key prefixing). But now I have - and it's a very simple change - you can do it since you're at it anyway: <img width="1188" height="380" alt="Image" src="https://github.com/user-attachments/assets/7f45380e-273e-4449-b26e-506c7c0f96ab" />
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

I think setting it in the constructor is a bit better as it avoids the first level lookups:

Image

Whatever you like..

@softworkz commented on GitHub (Nov 11, 2025): I think setting it in the constructor is a bit better as it avoids the first level lookups: <img width="1106" height="302" alt="Image" src="https://github.com/user-attachments/assets/bd2969c5-3759-4fce-a833-90e8e23d0450" /> Whatever you like..
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

I used it in methods? I thought I used CallMethodX for methods, no? But honestly I don't remember all cases, I'm doing so many things in parallel...

Yes that was part of your refactoring, we discussed different endings for 'completed' at the time.

I will make code changes.

@agracio commented on GitHub (Nov 11, 2025): >I used it in methods? I thought I used CallMethodX for methods, no? But honestly I don't remember all cases, I'm doing so many things in parallel... Yes that was part of your refactoring, we discussed different endings for 'completed' at the time. I will make code changes.
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

I will make code changes.

Perfect - then we should be even with the fork at minimum - probably better...

(I mean regarding this specific apart - otherwise we're anyway)

@softworkz commented on GitHub (Nov 11, 2025): > I will make code changes. Perfect - then we should be even with the fork at minimum - probably better... (I mean regarding this specific apart - otherwise we're anyway)
Author
Owner

@agracio commented on GitHub (Nov 11, 2025):

Could you explain the importance of Guids in the methods I posted above, why not just use Id?

Another desirable addition would be inclusion of cancelation tokens for tasks that use them but thats a much bigger addition.

@agracio commented on GitHub (Nov 11, 2025): Could you explain the importance of Guids in the methods I posted above, why not just use Id? Another desirable addition would be inclusion of cancelation tokens for tasks that use them but thats a much bigger addition.
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

Could you explain the importance of Guids in the methods I posted above, why not just use Id?

Gregor has done this in 412f628422 in 2019.

I can only speculate that it was done to mitigate the concurrency issues that we are still fighting against and that these might have been very apparent in this case because it takes a long time to return.

@softworkz commented on GitHub (Nov 11, 2025): > Could you explain the importance of Guids in the methods I posted above, why not just use Id? Gregor has done this in 412f62842207351627cf75f31bdff4402e5c3a1d in 2019. I can only speculate that it was done to mitigate the concurrency issues that we are still fighting against and that these might have been very apparent in this case because it takes a long time to return.
Author
Owner

@softworkz commented on GitHub (Nov 11, 2025):

Or maybe because the Id is actually a window id and not really identifying a session.

@softworkz commented on GitHub (Nov 11, 2025): Or maybe because the Id is actually a window id and not really identifying a session.
Author
Owner

@agracio commented on GitHub (Nov 12, 2025):

Ready for review: https://github.com/ElectronNET/Electron.NET/pull/924

@agracio commented on GitHub (Nov 12, 2025): Ready for review: https://github.com/ElectronNET/Electron.NET/pull/924
Author
Owner

@FlorianRappl commented on GitHub (Nov 13, 2025):

Merged - let's test this and publish the 0.1.0 tomorrow.

My proposal is from this on to pretty much release whenever we have something new (i.e., do fixes such as a 0.1.1, or add new features, such as a 0.2.0) quite quickly.

I'd go for a 1.0.0 release of this as early as ~mid of Jan. 2026 (unless we find some critical things). This should be sufficient time to get some user input and have enough experience to call it stable.

@FlorianRappl commented on GitHub (Nov 13, 2025): Merged - let's test this and publish the 0.1.0 tomorrow. My proposal is from this on to pretty much release whenever we have something new (i.e., do fixes such as a 0.1.1, or add new features, such as a 0.2.0) quite quickly. I'd go for a 1.0.0 release of this as early as ~mid of Jan. 2026 (unless we find some critical things). This should be sufficient time to get some user input and have enough experience to call it stable.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/Electron.NET#1008