NetCore IIS scoped requests - Sage BOI - Cannot initiate multiple "SY_Session"

SOLVED

HI All!

We've put together a netcore API hosted on IIS for integrating with other tools we have.  All of our services on the API are scoped to give the requests their own instances and dependencies.  

public static void AppServices(this IServiceCollection services)
        {
            services.AddScoped<IARInvoice, ARInvoice>();
            services.AddScoped<IGLTransJournal, GLTransJournal>();
            services.AddScoped<ICustDoc, CustDoc>();
            services.AddScoped<ICustomer, Customer>();
            services.AddScoped<IProvideXSession, ProvideXSession>();

        }

We have a ProvideX service class that instantiates the session object and allows for that services to utilize that session and make it's svc/ bus object requests to Sage BOI.  Because the netcore services are scoped and the ProvideX service is IDisposable, when the scoped service ends, it will run the Dispose() on the ProvideX dependency and drop the objects etc. for that session.

public void Dispose()
        {
            pvxSession.nCleanUp();
            pvxSession.DropObject();
            pvxSession = null;
        }

My issue is that even though each request is supposed to instantiate it's own ProvideXSession class and generate a new "SY_Session" object, it crashes as if it does not allow a new session object to be created while another is open from that same machine.  It is waiting for another session to close, then it will initialize another session object.  Because of the way the API is bringing in simultaneous requests that need to be handled in parallel, how can we allow multiple session with the same application user logon from the same IIS App Pool?

Here is the session request.  Again, even though these are scoped and completely separate requests to create "SY_Session" objects, it will fail on with a "System.Runtime.InteropServices.COMException (0x80020006): Unknown name. (0x80020006 (DISP_E_UNKNOWNNAME))" when a second parallel request is made. 

using Microsoft.Extensions.Configuration;
using System;
using ProvideX;


namespace SageAPI.Data
{
    public class ProvideXSession : IProvideXSession, IDisposable
    {
        private readonly IConfiguration _config;
        internal dynamic _pvxSession;
        internal Script _script;

        public dynamic pvxSession
        {
            get { return _pvxSession;  }
            set { _pvxSession = value; }

        }

        public ProvideXSession(IConfiguration config)
        {
            _config = config;
            _script = new Script();
            _script.Init(_config["AppSettings:SageMas90Folder"]);
            CreateSession(DateTime.Now.ToString("yyyyMMdd"));
        }

        private void CreateSession(string moduleDate)
        {

            pvxSession = _script.NewObject("SY_Session");
            pvxSession.nInitiateUI(); //<---- fails here when a newly instaited class make a request and there is an open session to Sage
            pvxSession.oUI.nInitCustomizer();
            pvxSession.nSetUser(_config["AppSettings:SageServiceUser"], _config["AppSettings:SageServiceSecret"]);
            pvxSession.nSetCompany(_config["AppSettings:SageCompany"]);
            pvxSession.nSetModule("SYS");
            pvxSession.nSetDate("SYS", moduleDate);

        }

        public dynamic CreateProgramInterface(string program, string module, DateTime moduleDate, string bus)
        {
            pvxSession.nSetDate(module, moduleDate.ToString("yyyyMMdd"));
            pvxSession.nSetModule(module);
            pvxSession.nSetProgram(pvxSession.nLookupTask(program));
            return pvxSession.oNewObject(bus);
        }
        public void Dispose()
        {
            pvxSession.nCleanUp();
            pvxSession.DropObject();
            pvxSession = null;
        }

    }
}


How can I get each API request to have open and close it's own session object to Sage without needing to wait for another session to drop.  Is there a setting in Sage that allows multiple sessions to be create from the same application user?

Parents
  • +1
    verified answer

    The issue is not with multiple session instances, but with the way C# caches the COM dispatch ID's within an AppDomain. This caching mechanism causes issues when applied to COM instances that are dynamic in nature, such as the case here. A simplified explanation:

    First Script Instance
    1. NewObject("SY_Session") returns IPvxDispatch interface which wraps the SY_Session BOI instance.
    2. "nInitiateUI" is called, forcing C# to resolve the method name to an ID.
    3. Internally, the Script instance generates a dynamic ID for the method name and saves this name/id mapping.
    4. The "nInitiateUI" method is then invoked using the ID, which the Script instance has a name mapping for.

    Subsequent Script Instances in the same AppDomain
    1. NewObject("SY_Session") returns IPvxDispatch interface which wraps the SY_Session BOI instance.
    2. "nInitiateUI" is called, and C# determines it has cached the ID for the method "nInitiateUI" on the IPvxDispatch interface type.
    4. The "nInitiateUI" method is then invoked using the cached ID. Because step 3 is skipped, the Script instance has no name mapping for the ID being passed, resulting in a DISP_E_UNKNOWNNAME.

    The solution is to force C# to always resolve the method/property name to an ID, vs. attempting to use the cached ID which we dynamically generate on the Script side. To handle cases such as this, we published a NuGet package called "Sage.Dispatch". Using the Package Manager Console in VS, you can add this package to your solution:

    PM> Install-Package Sage.Dispatch

    With this package, you can wrap the Script instance in a new ComDynamic() and it will handle the rest. I am including an updated version of your ProvideXSession class that demonstrates the usage.

    Hope this helps,
    Russell

        /// <summary>
        /// ProvideX session class wrapper.
        /// </summary>
        public class ProvideXSession : IProvideXSession, IDisposable
        {
            #region Private fields
    
            private readonly IConfiguration _config;
            private dynamic _pvxSession;
            private dynamic _script;
            private bool _disposed;
    
            #endregion
    
            #region Private methods
                
            /// <summary>
            /// Creates the supporting session instance.
            /// </summary>
            /// <param name="moduleDate">The module date to use.</param>
            private void CreateSession(string moduleDate)
            {
                _pvxSession = _script.NewObject("SY_Session");
                _pvxSession.nInitiateUI();
                _pvxSession.oUI.nInitCustomizer();
                _pvxSession.nSetUser(_config["AppSettings:SageServiceUser"], _config["AppSettings:SageServiceSecret"]);
                _pvxSession.nSetCompany(_config["AppSettings:SageCompany"]);
                _pvxSession.nSetModule("SYS");
                _pvxSession.nSetDate("SYS", moduleDate);
            }
    
            /// <summary>
            /// Resource cleanup.
            /// </summary>
            /// <param name="disposing">True if being disposed, otherwise false.</param>
            private void Dispose(bool disposing)
            {
                if (!disposing || _disposed) return;
    
                try
                {
                    _pvxSession?.nCleanUp();
                    _pvxSession?.FinalDropObject();
                    _script?.Dispose();
                }
                finally
                {
                    _disposed = true;
                    _pvxSession = null;
                    _script = null;
                }
            }
            
            #endregion
    
            #region Constructor
    
            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="config">The configuration instance.</param>
            public ProvideXSession(IConfiguration config)
            {
                _config = config ?? throw new ArgumentNullException(nameof(config));
                _script = new ComDynamic("ProvideX.Script");
                _script.Init(_config["AppSettings:SageMas90Folder"]);
    
                CreateSession(DateTime.Now.ToString("yyyyMMdd"));
            }
    
            #endregion
    
            #region Public methods
            
            /// <summary>
            /// Resource cleanup.
            /// </summary>
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
    
            /// <summary>
            /// Creates an instance of the BOI object.
            /// </summary>
            /// <param name="program">The program name.</param>
            /// <param name="module">The module code.</param>
            /// <param name="moduleDate">The date for the specified module.</param>
            /// <param name="bus">The business object class.</param>
            /// <returns>The instance of the BOI object.</returns>
            public dynamic CreateProgramInterface(string program, string module, DateTime moduleDate, string bus)
            {
                _pvxSession.nSetDate(module, moduleDate.ToString("yyyyMMdd"));
                _pvxSession.nSetModule(module);
                _pvxSession.nSetProgram(_pvxSession.nLookupTask(program));
    
                return _pvxSession.oNewObject(bus);
            }
    
            #endregion
    
            #region Public properties
    
            /// <summary>
            /// Returns the session instance.
            /// </summary>
            public dynamic PvxSession
            {
                get { return _pvxSession; }
            }
    
            #endregion
        }

  • 0 in reply to Russell Libby

    Got it! Great explanation, thank you.  That was it, the COM objects were caching. I was able to get the package and apply the wrapper.  The COM Object is processing the separate requests like it should now with no errors.

    Thank you!

Reply Children
  • 0 in reply to nsaia



    This was ok for a couple of days, but Sage.Dispatch keeps throwing an error and will then cause every call to the COM Object afterwards to fail until we reset IIS.  

    Sage.Dispatch.ComDynamicException: Retrieving the COM class factory for component with CLSID {60503AB4-2D27-11D6-B249-00C04F50D575} failed due to the following error: 80080005 Server execution failed (0x80080005 (CO_E_SERVER_EXEC_FAILURE)).

  • 0 in reply to nsaia

    Hard to say why without seeing the full code in question. Anyways, I would first check to make sure that the instances of PvxCom.exe are getting released when your request processing completes. These processes can be viewed in the details tab of Task Manager. If there are a number of these processes out there, then proper cleanup is not happening. If this is the case, then you would need to review your code to ensure that Dispose is called on all allocated instances when your processing completes. Also, is your application pool set to recycle? To determine if recycling will help, the next time this error occurs, try manually recycling the application pool vs restarting/resetting IIS. 

    Russell

  • 0 in reply to Russell Libby

    Hi Russell,

    Yes.  Code is the same as above.  The ProvideXSession interface inherits IDisposable, Services in our netCore are scoped and providexSession (also scoped) is dependency injected on our service.  When the service class is finished with the call, the dependencies that are IDisposable get the Dispose() called.  We see this when debugging that that dispose and cleanup is firing.  However, We still see the PvxCom.Exe tasks in the Task Manager as they are being created, but not released.  

  • 0 in reply to nsaia

    Tested the ProvideXSession code again, and I missed two things that kept pvxcom.exe from releasing. First, the use of 'oUI' places a ref count against the script instance but it's never captured, so never disposed. This should change to:

            private void CreateSession(string moduleDate)
            {
                _pvxSession = _script.NewObject("SY_Session");
                _pvxSession.nInitiateUI();
    
                /* Capture and dispose of the COM instance */
    		    using (dynamic ui = _pvxSession.oUI) { ui.nInitCustomizer(); }
    
                _pvxSession.nSetUser(_config["AppSettings:SageServiceUser"], _config["AppSettings:SageServiceSecret"]);
                _pvxSession.nSetCompany(_config["AppSettings:SageCompany"]);
                _pvxSession.nSetModule("SYS");
                _pvxSession.nSetDate("SYS", moduleDate);
            }

    And the other item I missed was the dispose of _pvxSession; my apologies. 

           /// <summary>
            /// Resource cleanup.
            /// </summary>
            /// <param name="disposing">True if being disposed, otherwise false.</param>
            private void Dispose(bool disposing)
            {
                if (!disposing || _disposed) return;
    
                try
                {
                    _pvxSession?.nCleanUp();
                    _pvxSession?.FinalDropObject();
                    _pvxSession?.Dispose(); /* Missed this before */
                    _script?.Dispose();
                }
                finally
                {
                    _disposed = true;
                    _pvxSession = null;
                    _script = null;
                }
            }

    Without these changes, I was seeing orphaned pvxcom.exe instances in Task Manager. With the changes in place, cleanup and release of pvxcom.exe was working as expected.

    Let me know how it goes,

    Russell

  • 0 in reply to Russell Libby

    Hi Russell,

    The PvxCOM.exe orphaned tasks are still there after applying those changes.  The objects are being deallocated and I can drop the PvxCom.exe objects with a GC.Collect, but I don't want to do that as this will be firing for every request that completes.  The _pvxSession?.Dispose() should be working.  I have the GC.SuppressFinalize(this) after the Dispose is ran, but the PvxCom tasks are still present.

  • 0 in reply to nsaia

    Something is amiss if a call to GC.Collect() is cleaning up the pvxcom.exe instances. And I agree, you should not be calling GC.Collect(), at all. From the IProvideXSession side of things everything looks correct, and I have verified that cleanup occurs as expected. But I still don't have the full picture on this as I have no idea what your other DI classes are doing. It would be helpful to see the actual code for at least one of these. E.g. IARInvoice, ARInvoice. 

  • 0 in reply to Russell Libby

    Just to close this thread out, the nCleanup method for SY_Session was reporting an error 42 which resulted in an exception on the .NET side, causing the remaining disposal code to be skipped. The solution was to wrap the methods with try/catch.


            /// <summary>
            /// Resource cleanup.
            /// </summary>
            /// <param name="disposing">True if being disposed, otherwise false.</param>
            private void Dispose(bool disposing)
            {
                if (!disposing || _disposed) return;
    
                try
                {
                    try { _pvxSession?.nCleanUp(); } catch { }
                    try { _pvxSession?.FinalDropObject(); } catch { }
                    try { _pvxSession?.Dispose(); } catch { }
                    try { _script?.Dispose(); } catch { }
                }
                finally
                {
                    _disposed = true;
                    _pvxSession = null;
                    _script = null;
                }
            }