dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.98k stars 4.66k forks source link

Windows ComHost COM registration in HKCU instead of HKLM? #45750

Open smourier opened 3 years ago

smourier commented 3 years ago

As far as I know, currently, to register a COM object written in .NET 5 (or .NET Core 3.x for that matter) we need to

"Open an elevated command prompt and run regsvr32 ProjectName.comhost.dll. That will register all of your exposed .NET objects with COM." from here

https://docs.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com#register-the-com-host-for-com

I think the relevant source is here: https://github.com/dotnet/runtime/blob/master/src/installer/corehost/cli/comhost/comhost.cpp#L309 it seems pretty much hardcoded for HKEY_LOCAL_MACHINE.

As you know, registering COM component in HKCU (HKEY_CURRENT_USER\SOFTWARE\Classes\CLSID for the CLSID, etc.) is pretty common and supported by "COM" in general (CoCreateInstance, etc.). It's also supported by tooling such as Visual Studio's ATL and Windows' regsvr32.exe with the help of DllInstall (optional) DLL export:

https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-dllinstall

It has the enormous advantage of avoiding elevated rights for setup, installers, etc.

Can support for this be added to comhost?

In the meantime, is there any way to do this with current .NET 5? Should I write a custom comhost? How?

Wraith2 commented 3 years ago

I'm aware that registering com servers in the user hive functions but is it a supported configuration? You cite using DllInstall but that simply allows you to integrate custom registration logic it doesn't provide you a supported way to register user rather than local machine level servers.

As far as I'm aware registration free com using manifests is the suggested way of dealing with registration elevation permissions requirement issues.

smourier commented 3 years ago

Supported by who? what? As I said, yes, it's supported by CoCreateInstance, and COM/Windows in general, including all Shell API (Explorer, Common Dialogs, etc.).

I know DllInstall in itself doesn't allow anything special, I'm not specifically attached to that, but try Visual Studio ATL's and you'll see that's how it supports HKCU registration in the wizard-generated code:

// DllInstall - Adds/Removes entries to the system registry per user per machine.
STDAPI DllInstall(BOOL bInstall, _In_opt_  LPCWSTR pszCmdLine)
{
    HRESULT hr = E_FAIL;
    static const wchar_t szUserSwitch[] = L"user";

    if (pszCmdLine != nullptr)
    {
        if (_wcsnicmp(pszCmdLine, szUserSwitch, _countof(szUserSwitch)) == 0)
        {
            ATL::AtlSetPerUserRegistration(true);
        }
    }

    if (bInstall)
    {
        hr = DllRegisterServer();
        if (FAILED(hr))
        {
            DllUnregisterServer();
        }
    }
    else
    {
        hr = DllUnregisterServer();
    }

    return hr;
}

registration-free means you must distribute the components with the clients. This answers different scenarios.

AaronRobinsonMSFT commented 3 years ago

I'm aware that registering com servers in the user hive functions but is it a supported configuration?

At present, it is not supported in .NET Core 3.0+ or .NET 5. COM registration is only supported for the HKLM hive. This was chosen for security reasons. In .NET Framework the behavior had issues that were not appropriate and relied upon legacy registry behavior from the Windows 95/98 days. We are now explicit about this and force HKLM.

As far as I'm aware registration free com using manifests is the suggested way of dealing with registration elevation permissions requirement issues.

Yes. That would be our suggested workaround.

registration-free means you must distribute the components with the clients. This answers different scenarios. Can support for this be added to comhost?

@smourier Your concerns with RegFree COM are valid and I empathize with the issue. I don't personally think we should be continuing some of the questionable COM practices of yore, but do recognize there are applications that have been built up around this option. We can absolutely add support for it, as you discovered it is hardcoded and wouldn't be technically difficult to add a flag or some metadata that indicates where one would like the registration to go. When it was implemented the "most common" scenario was selected - not a lot more thought put into it.

Feel free to propose a mechanism in this issue's description or we can prioritize it as is with other interop feature requests. I would imagine a new attribute would suffice or perhaps extend ComRegisterFunctionAttribute? Both of these would have to go through the official .NET API review - I am more than willing to help shepherd them. Non-API impacting suggestions are possible as well (i.e. environment variable during registration or modify .clsidmap generation, we would likely need an API for the later). I haven't given any of these options much though so feedback/initiative from you or the community as to direction would be appreciative to start the process.

smourier commented 3 years ago

Thanks for your answer.

If such a feature is added, it's very important to decide where registration happens at deployment time vs compilation time. This is what the ATL-generated code does.

So, my initial idea was really not to change any .NET API but instead add support in comhost for a DllInstall parameter that would instruct the registration to happen in HKCU instead of HKLM. So, COM-support related code stays in COM-support related binaries, impact on .NET is minimal.

Since .NET 5 requires each COM coclass guid to be specified, I could even imagine a DllInstall command that could be able to support one component in HKCU, another in HKLM, but maybe that's too complicated. By default, all components would be in HKCU or all in HKLM.

regsvr32 (which supports DllInstall) is very well known and in fact already referenced in the .NET Core doc here: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.comregisterfunctionattribute?view=net-5.0) but that would support custom installers calling DllInstall too.

Wraith2 commented 3 years ago

Adding the ability to perform user registrations would also have an effect on tools that generate or gather registration information. For example when I last used WiX to generate installers the official advice was not to try and invoke DllInstall dynamically at install time but to do a gather step and embed the resulting registry alterations. Properly written this won't change if registration if in hkcu instead of hklm but I expect there to be improperly written apps out there that only redirect hklm because that's all that was supported. This is why I asked whether it was a supported scenario, while I know per-user registration works if it isn't supported it can put consumers in a situation where they are doing something that could cause a support ticket to be rejected.

For clarity, I like the idea and I think extending ComRegisterFunctionAttribute with a per-user flag would be a nice way to do it, but I also think there will be ecosystem knock-on effects.

govert commented 3 years ago

@AaronRobinsonMSFT You say:

In .NET Framework the behavior had issues that were not appropriate and relied upon legacy registry behavior from the Windows 95/98 days.

Do you have some more information or a reference about these issue?

The registry behaviour under the Office Click-to-Run environment (I think it is related to App-V) has also been problematic in recent Office versions, with undocumented registry types and unexpected behaviour in different versions.

In general the component developer has little control over the environment into which this must be installed or run, so so some flexibility about the registration is very valuable - when it runs, registry vs. SxS activation contexts, and some HKCU plan if possible.

AaronRobinsonMSFT commented 3 years ago

In the meantime, is there any way to do this with current .NET 5? Should I write a custom comhost? How?

Missed this query. It is entirely possible and one could use the existing source to start. One missing aspect from the source is the need to link against our public low level host API (i.e. nethost.lib). A sample exists for that here.

regsvr32 (which supports DllInstall) is very well known and in fact already referenced in the .NET Core doc here: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.comregisterfunctionattribute?view=net-5.0) but that would support custom installers calling DllInstall too.

@smourier Yep. Support for COM in .NET Core 3+ and .NET 5 was designed to make the UX align with long standing COM tooling. This may be a difficult change in the short term but we are hoping it aligns better with long standing COM developers as opposed to .NET providing an alternative COM registration methodology. Your suggestion to use DllInstall aligns with that goal.

I personally have no issue with enabling DllInstall support, but as @Wraith2 said the UX is going to require some thought.

For example when I last used WiX to generate installers the official advice was not to try and invoke DllInstall dynamically at install time but to do a gather step and embed the resulting registry alterations.

@Wraith2 Yes that is indeed the correct advice. WiX and MSIs handle the reference counting logic for the installation whereas regsvr32 doesn't. I agree with your general sentiment here and before we support this we would need to investigate some existing use patterns to ensure existing developers can still accomplish their goals.

Do you have some more information or a reference about these issue?

@govert Unfortunately those details aren't something I am permitted to discuss for various reasons. I can say we are now explicitly registering in the HKLM hive to ensure we know where the server is registered. This provides explicit behavior that aligns with work done in the OS during the Windows Vista time frame. I really can't comment further.

lauxjpn commented 3 years ago

The traditional way to write the COM related registry keys in the HKCU hive on demand is to use the OaEnablePerUserTLibRegistration and RegOverridePredefKey functions.

Once you have moved the registry context to HKCU, you would just load the YourApp.comhost.dll via LoadLibrary and call its DllRegisterServer or DllUnregisterServer entry point.

All HKCR related keys are then written to HKCU\Software\Classes instead of HKLM\Software\Classes.


While regsvr32.exe never had a user-level registration feature, there are tools out there that do this, in case you don't want to implement it yourself. One of those tools is RegSvrEx, though it does not seem to call OaEnablePerUserTLibRegistration (might be worth a PR).

I've also got some .NET code I could publish (just in case anybody is interested at some point in the future), which does all that and that I have been using for years in scenarios, where Reg-Free-COM via manifests is not an option (e.g. when you want to load components into Office applications, but can't get elevated and of course can't just modify the manifest file of the Office app).


Feel free to propose a mechanism in this issue's description or we can prioritize it as is with other interop feature requests. I would imagine a new attribute would suffice or perhaps extend ComRegisterFunctionAttribute?

@AaronRobinsonMSFT I would also suggest a new attribute (or attribute pair). I am also interested in that, because of .NET's current lack of TLB generation. So when I manually generate a TLB from an IDL file via MIDL.exe, I want to register the type library during the standard registration process. This is currently only possible by using the ComRegisterFunctionAttribute, which is conceptionally not the right place to do this (especially with multiple COM classes in one assembly).

AaronRobinsonMSFT commented 3 years ago

This is currently only possible by using the ComRegisterFunctionAttribute, which is conceptionally not the right place to do this (especially with multiple COM classes in one assembly).

@lauxjpn I'm not sure I follow this. The function marked with ComRegisterFunctionAttribute is immediately following registration so I am curious where you feel the right place for this? Would your suggestion be something akin to TlbRegisterFunctionAttribute?

lauxjpn commented 3 years ago

Would your suggestion be something akin to TlbRegisterFunctionAttribute?

@AaronRobinsonMSFT Something like that. It does not necessarily have to be tied to the TLB, but it should be called only once per DllRegisterServer/DllUnregisterServer call.

Maybe an additional attribute can be introduced, that could then be placed on a class, marking it as a [ComRegistrationClass] or something similar. Then the [ComRegisterFunction] and [ComUnregisterFunction] attributes could be reused in this context on the classes methods. The class level attribute could be restricted to static classes. Enumerating the decorated classes in ComActivator should not be much of a performance concern, since registration is usually not done on a hot path (and usually even outside of the normal application runtime).

A more modern approach would be to use something convention based, but this would break with the pattern to use attributes, that has been established for COM in .NET about 20 years ago, so its probably not a good idea.

AaronRobinsonMSFT commented 3 years ago

but it should be called only once per DllRegisterServer/DllUnregisterServer call.

So I could be mistaken here, but a function with that attribute is only called once during the respective DllRegisterServer/DllUnregisterServer call. How would the above suggestion differ in this case?

The class level attribute could be restricted to static classes. Enumerating the decorated classes in ComActivator should not be much of a performance concern, since registration is usually not done on a hot path (and usually even outside of the normal application runtime).

This would be creating a lot of new policy for a system that we don't believe will continue to evolve much. Is there a perception that this approach is something that will grow in popularity which would make investment important or slowly decrease as time progresses? Office - which has historically been the most popular .NET COM interop consumer - seems to focus far more on JavaScript Add-Ins.

Note: I am not suggestion COM or Office COM extensions aren't important. I am trying to understand the community perspective and if this is a growing area to invest in or if this area may not warrant time to prioritize improvements in.

lauxjpn commented 3 years ago

So I could be mistaken here, but a function with that attribute is only called once during the respective DllRegisterServer/DllUnregisterServer call. How would the above suggestion differ in this case?

There are two main differences from my point of view here:

Is there a perception that this approach is something that will grow in popularity which would make investment important or slowly decrease as time progresses?

On the Win32 level, COM is still the number one choice to implement new features and from a .NET perspective, it is the glue technology to integrate your application/components with any (well, most) non-.NET applications/components on Windows.

Since .NET 5 does not provide integrated type library support, manually generating type libraries and registering them will become much more common in the future.

Office - which has historically been the most popular .NET COM interop consumer - seems to focus far more on JavaScript Add-Ins.

While this is true, Microsoft has introduced many new technologies for their products over the years, while only very few of them usually stick. In regards to Office, there is an over 20 year history of VBS and COM component consumption, so even if JavaScript Add-Ins will become popular of the next few years, there are so many components and applications out there relying on COM, that first-class support will need to be part of .NET on the traditional Windows platform forever anyway. Microsoft is not focusing too much specifically on COM for their applications, because it just works and most traditional Microsoft applications (like the Office applications) are COM hosts anyway, so they are based on COM and therefore there is not much focus needed.


If .NET 5 would have type library support, this would be less of an issue. But since it hasn't, the need to customize your COM classes and application registrations has significantly increased.


This would be creating a lot of new policy for a system that we don't believe will continue to evolve much.

I am not sure, that this is really the case. Reusing the same attributes for the same purpose on a different level seems intuitive to me. It would not be a compatibility issue and pretty much in line with how the COM interop implementation handles similar issues. It would also be easy to discover, since the additional usage scenario would be mentioned in the docs for the same attribute that has been used (and misused) for 20 years for similar cases. The .NET implementation would be pretty simple too and change very little of the already established code. So from my point of view, there is an increasing need with little risk.

(I personally am more of a fan of convention based approaches, where ComActivator would just look for a specific registration class and call specific methods on it, which is easily extendable in the future, if additional hooks are needed. This implementation would also be distinct from the .NET Framework one, so its light on the docs changes. But its not what anyone familiar with COM interop would expect and is hard to discover.)

AaronRobinsonMSFT commented 3 years ago

Using the current mechanism, that is conceptionally bound to a particular COM class, to perform actions that are not about this COM class (or not just this COM class), is a hack (or at least a semantic issue).

Fair point. So the idea here would be at the assembly level not specifically tied to an exported COM class. Message received, I agree with your perspective on it being semantically confusing. This argument is convincing given how much work it would be to consider support - limited by our other priorities of course.

On the Win32 level, COM is still the number one choice to implement new features Since .NET 5 does not provide integrated type library support, manually generating type libraries and registering them will become much more common in the future.

We are not entirely in agreement here. The WinRT API surface area is where new APIs are appearing. WinRT is built on top of COM so there is overlap but the system is fundamentally different and registration is different - TLBs don't exist. In addition to that the C#/WinRT repo is working to provide a .NET component authoring story. I would argue that is where we should invest rather than the built-in COM system.

In regards to Office, there is an over 20 year history of VBS and COM component consumption, so even if JavaScript Add-Ins will become popular of the next few years, there are so many components and applications out there relying on COM, that first-class support will need to be part of .NET on the traditional Windows platform forever anyway.

I fully agree with respect to .NET Framework and that is also our statement as it relates to COM interop. However, for .NET 5+ this is not a specific goal as it relates to the Office ecosystem. There are many issues with the Office COM model and .NET 5+ and it is not clear if Office will fully support .NET 5+ until multiple versions of the runtime can be loaded in the same process - which is not a goal at present - see https://github.com/dotnet/runtime/issues/45285#issuecomment-736712591. I think until Office reprioritizes the COM extension approach or another scenario pushes us toward enabling multiple in-proc runtime instances the best advice here is to focus on .NET Framework development for Office extensions.

lauxjpn commented 3 years ago

So the idea here would be at the assembly level not specifically tied to an exported COM class.

Yeah, that's it.

The WinRT API surface area is where new APIs are appearing.

That's definitely the case for APIs in general. (I was really only talking about the Win32 layer here.)

I think until Office reprioritizes the COM extension approach or another scenario pushes us toward enabling multiple in-proc runtime instances the best advice here is to focus on .NET Framework development for Office extensions.

I agree that for the time being, writing .NET Standard 2.0 compatible net48 code is probably the best choice for Office compatible COM classes. Over time, when the gap between .NET Standard 2.0 and .NET 5+ widens, the impact level of the issue will become more clear and whether its worth investing in that area or not.

bclothier commented 3 years ago

I want to add my support / vote for this request. We usually prefer using HKCU over HKLM since the addins we provide for use with Office usually are scoped for users and needs to be available to corporate uses who may have a locked down environment and thus cannot write to HKCU.

Manifest approach is not practical because that requires creating a manifest associated with an .exe file for the Office which will require admin privilege anyway AND it is also improper because it makes the scope is to the all documents managed by the Office when in fact it may only apply to certain kind of documents (e.g. a COM addin/library is used by only one specific Excel spreadsheet or a particular Access database).

The workaround is to manually write a .reg script that simulates the registry keys created as a result of regsvr32 but that's tedious & errorprone.

lauxjpn commented 3 years ago

@bclothier The general way to accomplish what you want, is to register your in-proc DLL after calling RegOverridePredefKey.

For type library registrations, also call OaEnablePerUserTLibRegistration.

(If necessary, I can probably share some C# code I have been using for about 10 years to register COM libraries under HKCU, so they can be registered and loaded from a normal user context without elevation. I have used it also for COM and ActiveX components that needed to be loaded in Excel or Access).

bclothier commented 3 years ago

@lauxjpn Thank you for the insight! This is very good to know but I'm a bit fuzzy on one point. Given that we call the regsvr32.dll, which in turn calls the ComDllRegisterFunction-decorated function, wouldn't it be necessary to run RegOverridePredefKey before running the regsvr32.dll lest it registers stuff which happens before we enter the custom function?

lauxjpn commented 3 years ago

@bclothier Not sure what the regsvr32.dll file is you are mentioning.

The general procedure would look like this:

LoadLibrary("Oleaut32.dll")
    GetProcAddress("OaEnablePerUserTLibRegistration")
    Marshal.GetDelegateForFunctionPointer()
    Call OaEnablePerUserTLibRegistration()

    key = RegOpenKey(RegistryHive.CurrentUser, @"Software\Classes")
        RegOverridePredefKey(RegistryHive.ClassesRoot, key)
    RegCloseKey(key)

        LoadLibrary("YourLibrary.comhost.dll")
            GetProcAddress("DllRegisterServer")
            Marshal.GetDelegateForFunctionPointer()
            Call DllRegisterServer()
        FreeLibrary()

    RegOverridePredefKey(RegistryHive.ClassesRoot, null)
FreeLibrary()
bclothier commented 3 years ago

Sorry, I mistyped. I meant regsvr32.exe, not regsvr32.dll. The process you outlined sounds like this needs to be run in lieu of the regsvr32.exe whereas Microsoft says that you can just call regsvr32.exe on your COM-enabled project and it just works™. The ComRegisterFunction attribute is called by the regsvr32.exe but after it has done its own registration.

Given that regsvr32.exe does not officially support per-user registration, this might be the next best alternative.

lauxjpn commented 3 years ago

The regsvr32.exe YourLibrary.comhost.dll call just loads the YourLibrary.comhost.dll and then calls the DllRegisterServer entry point. It doesn't know anything about .NET or its attributes (but the auto-generated X.comhost.dll does).

The procedure I outlined does not depend on regsvr32.exe. You could implement it in your own exe-Tool and use that one for registering/unregistering your components instead of regsvr32.exe. You could also move the code to a .NET class and call that class' method from VBA to register your components. If you have some kind of setup routine, you could run the procedure from there.

How you execute the procedure is really up to you and depends on how you run and deploy your app or components.

marklechtermann commented 2 years ago

If you are looking for a way to generate a TLB from a .NET 5+ assembly, this project might help you: https://github.com/dspace-group/dscom

AaronRobinsonMSFT commented 2 years ago

@marklechtermann This is a very interesting project! Thank you for sharing and providing a community solution for TLB tooling in .NET 5+.

smourier commented 2 years ago

Building a TLB from .NET 5+ is not the subject of this issue, but anyway, IMHO, this should be provided by Microsoft like it used to be, not by the community.

gusmally commented 1 year ago

(If necessary, I can probably share some C# code I have been using for about 10 years to register COM libraries under HKCU, so they can be registered and loaded from a normal user context without elevation. I have used it also for COM and ActiveX components that needed to be loaded in Excel or Access).

@lauxjpn I would be very interested in seeing this code!

lauxjpn commented 1 year ago

@lauxjpn I would be very interested in seeing this code!

@gusmally Here is the COM library registration code that I have been using for a long time. It is able to register libraries for the current user:

COM library registration source code ```c# using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace Registration { public static class ComLibrary { #region External Declarations [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr LoadLibrary(string lpFileName); [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool FreeLibrary(IntPtr hModule); #endregion private delegate int ComLibraryFunctionDelegate(); private delegate void EnableTypeLibRegistrationForCurrentUserDelegate(); private static bool IsTypeLibRegistrationForCurrentUserEnabled { get; set; } public static bool Register(string libraryPath, bool currentUserOnly = false, TraceSource traceSource = null) { traceSource?.TraceInformation($"COM library \"{libraryPath}\" is being registered for {(currentUserOnly ? "the current user" : "all users")}."); var succeeded = false; if (currentUserOnly) { EnableTypeLibRegistrationForCurrentUser( () => { using (new UserClassesRegistryContext()) succeeded = CallLibraryFunction("DllRegisterServer", libraryPath, traceSource); }, traceSource); } else succeeded = CallLibraryFunction("DllRegisterServer", libraryPath, traceSource); traceSource?.TraceInformation($"The registration process {(succeeded ? "succeeded" : "failed")}."); return succeeded; } public static bool Unregister(string libraryPath, bool currentUserOnly = false, TraceSource traceSource = null) { traceSource?.TraceInformation($"COM library \"{libraryPath}\" is being unregistered for {(currentUserOnly ? "the current user" : "all users")}."); var succeeded = false; if (currentUserOnly) { EnableTypeLibRegistrationForCurrentUser( () => { using (new UserClassesRegistryContext()) succeeded = CallLibraryFunction("DllUnregisterServer", libraryPath, traceSource); }, traceSource); } else succeeded = CallLibraryFunction("DllUnregisterServer", libraryPath, traceSource); traceSource?.TraceInformation($"The unregistration process {(succeeded ? "succeeded" : "failed")}."); return succeeded; } private static void EnableTypeLibRegistrationForCurrentUser(Action action, TraceSource traceSource = null) { if (IsTypeLibRegistrationForCurrentUserEnabled) { action(); return; } traceSource?.TraceInformation("User specific type library registration will be used."); var hModule = IntPtr.Zero; const string libraryPath = "Oleaut32.dll"; const string functionName = "OaEnablePerUserTLibRegistration"; try { hModule = LoadLibrary(libraryPath); if (hModule == IntPtr.Zero) throw new InvalidOperationException($"The library \"{libraryPath}\" could not be loaded."); var libraryFunctionProcAddress = GetProcAddress(hModule, functionName); if (libraryFunctionProcAddress != IntPtr.Zero) { var libraryFunction = (EnableTypeLibRegistrationForCurrentUserDelegate)Marshal.GetDelegateForFunctionPointer(libraryFunctionProcAddress, typeof(EnableTypeLibRegistrationForCurrentUserDelegate)); libraryFunction(); } IsTypeLibRegistrationForCurrentUserEnabled = true; action(); } finally { IsTypeLibRegistrationForCurrentUserEnabled = false; if (hModule != IntPtr.Zero) { FreeLibrary(hModule); hModule = IntPtr.Zero; } } } private static bool CallLibraryFunction(string functionName, string libraryPath, TraceSource traceSource = null) { var hModule = IntPtr.Zero; try { hModule = LoadLibrary(libraryPath); if (hModule == IntPtr.Zero) throw new InvalidOperationException($"The library \"{libraryPath}\" could not be loaded."); var libraryFunctionProcAddress = GetProcAddress(hModule, functionName); if (libraryFunctionProcAddress == IntPtr.Zero) throw new InvalidOperationException($"The entry point \"{functionName}\" of COM library \"{libraryPath}\" could not be found."); var libraryFunction = (ComLibraryFunctionDelegate)Marshal.GetDelegateForFunctionPointer(libraryFunctionProcAddress, typeof(ComLibraryFunctionDelegate)); var hResult = libraryFunction(); traceSource?.TraceInformation($"The entry point \"{functionName}\" of COM library \"{libraryPath}\" returned an HRESULT value of \"{hResult}\"."); return hResult == 0; } finally { if (hModule != IntPtr.Zero) { FreeLibrary(hModule); hModule = IntPtr.Zero; } } } } public class UserClassesRegistryContext : IDisposable { #region External Declarations [DllImport("advapi32.dll", SetLastError = true)] private static extern int RegOpenKey(IntPtr hKey, string lpSubKey, out IntPtr phkResult); [DllImport("advapi32.dll", SetLastError = true)] private static extern int RegOverridePredefKey(IntPtr hkey, IntPtr hnewKey); [DllImport("advapi32.dll", SetLastError = true)] private static extern int RegCloseKey(IntPtr hKey); #endregion private const long ERROR_SUCCESS = 0; public bool InContext { get; private set; } public UserClassesRegistryContext() { EnterContext(); } ~UserClassesRegistryContext() { ReleaseUnmanagedResources(); } public void Dispose() { ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } private void ReleaseUnmanagedResources() { if (InContext) ExitContext(); } private void EnterContext() { if (RegOpenKey(new IntPtr((int) RegistryHive.CurrentUser), @"Software\Classes", out var hKey) != ERROR_SUCCESS) throw new InvalidOperationException(@"The registry key ""HKEY_CURRENT_USER\Software\Classes"" could not be opened."); try { if (RegOverridePredefKey(new IntPtr((int) RegistryHive.ClassesRoot), hKey) != ERROR_SUCCESS) throw new InvalidOperationException(@"The registry key ""HKEY_CLASSES_ROOT"" could not be overridden by ""HKEY_CURRENT_USER\Software\Classes""."); InContext = true; } finally { RegCloseKey(hKey); } } private void ExitContext() { if (!InContext) throw new InvalidOperationException("ExitContext was called before calling EnterContext first."); RegOverridePredefKey(new IntPtr((int)RegistryHive.ClassesRoot), IntPtr.Zero); InContext = false; } } } ```

Just call ComLibrary.Register(...) or ComLibrary.Unregister(...).