MarimerLLC / cslaforum

Discussion forum for CSLA .NET
https://cslanet.com
Other
31 stars 6 forks source link

Csla.ApplicationContext.User and SerializationException in Office add-ins (with demo zip) #411

Closed michaelcsikos closed 7 years ago

michaelcsikos commented 7 years ago

I am getting some weird behaviour trying to set Csla.ApplicationContext.User to a new Csla.Security.UnauthenticatedPrincipal or my own custom principal inside a Word add-in. I am using the Csla.ApplicationContextManager. All of this works perfectly in a standalone form.

As you can see from the following test method, the principal is an UnauthenticatedPrincipal at the start, and the message box displays correctly. After setting the current user to a new UnauthenticatedPrincipal I can Debug.WriteLine the current user, but anything displaying a MessageBox, or even new Form().ShowDialog() throws:

System.Runtime.Serialization.SerializationException: Type is not resolved for member 'Csla.Security.UnauthenticatedPrincipal,Csla, Version=4.6.500.0, Culture=neutral, PublicKeyToken=93be5fdc093e4c30'.

private void TestPrincipal1(object sender, RibbonControlEventArgs e)
{
    Csla.ApplicationContext.ContextManager = new Csla.ApplicationContextManager();

    MessageBox.Show("Before principal set: " + Csla.ApplicationContext.User);
    // Message box displays correctly:
    // "Before principal set: Csla.Security.UnauthenticatedPrincipal"

    Csla.ApplicationContext.User = new Csla.Security.UnauthenticatedPrincipal();

    Debug.WriteLine("After principal set: " + Csla.ApplicationContext.User);
    // Debug displays correctly:
    // "After principal set: Csla.Security.UnauthenticatedPrincipal"

    try
    {
        // The following message box does not display:
        MessageBox.Show("After principal set: " + Csla.ApplicationContext.User);
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
        // System.Runtime.Serialization.SerializationException:
        // Type is not resolved for member
        // 'Csla.Security.UnauthenticatedPrincipal,Csla, Version=4.6.500.0,
        // Culture=neutral, PublicKeyToken=93be5fdc093e4c30'.
    }
}

It gets even stranger, though. If I set the user inside a form, it works:

private void TestPrincipal2(object sender, RibbonControlEventArgs e)
{
    Csla.ApplicationContext.ContextManager = new Csla.ApplicationContextManager();

    MessageBox.Show("Before principal set: " + Csla.ApplicationContext.User);
    // Message box displays correctly:
    // "Before principal set: Csla.Security.UnauthenticatedPrincipal"

    var formLogIn = new FormLogIn();

    formLogIn.ShowDialog();

    try
    {
        MessageBox.Show("After principal set: " + Csla.ApplicationContext.User);
        // Message box displays correctly:
        // "After principal set: MyExample.OperatorPrincipal"
    }
    catch (Exception ex)
    {
        // No exception is thrown
    }
}

This has been driving me nuts today. I hope someone can shed some light on it.

Kind regards Michael

michaelcsikos commented 7 years ago

OK, more strange behaviour. If I leave out the very first MessageBox displaying "Before principal set", the rest works in TestPrincipal1. The "After principal set" message displays correctly.

michaelcsikos commented 7 years ago
private void ThisWorks()
{
    Csla.ApplicationContext.User = new Csla.Security.UnauthenticatedPrincipal();

    MessageBox.Show("After"); // Displays
}

private void ThisThrows()
{
    MessageBox.Show("Before");

    Csla.ApplicationContext.User = new Csla.Security.UnauthenticatedPrincipal();

    MessageBox.Show("After"); // SerializationException
}
michaelcsikos commented 7 years ago

I have updated to CSLA 4.6.603, but the behaviour is the same.

rockfordlhotka commented 7 years ago

I don't know what's going on, but I have a couple thoughts.

First, it is most likely that your code is using the default application context, which relies on the current thread for the current principal. Different host environments manage (or don't manage) the current principal in different ways. It could be that Word is doing something odd/different.

Second, some host environments (most notably unit testing frameworks) run some code in one appdomain and other code in another appdomain. This means the thread crosses appdomain boundaries, which means that things like the current thread's principal object gets serialized/deserialized across that boundary.

If the other side of the boundary (often the host) may not have access to the DLL containing the type. I don't think that's what you are seeing though. But it is also the case that the BinaryFormatter sometimes can't find the assembly, even though it is in memory. This happens in IE for example. In that case you need to use a workaround to force the deserialization process to successfully find the assembly.

It has been a long time since I've thought about that workaround code, so I don't have a quick pointer to it (it is just a few lines of code). But it was in the old NetRun utility from CSLA .NET 1.x. Try searching the CSLA codebase for "serialization workaround" and see if you can find it.

michaelcsikos commented 7 years ago

Thanks for your reply, Rocky.

In ThisAddIn_Startup I am doing the following:

Csla.ApplicationContext.ContextManager = new Csla.ApplicationContextManager();

SerializationWorkaround.Init();

We have been using the SerializationWorkaround code for almost 10 years because of problems with AutoCAD.

michaelcsikos commented 7 years ago

If I call the following method before any UI code, the problem seems avoidable:

public static void ApplicationContextUserWorkaround()
{
    Csla.ApplicationContext.User = Csla.ApplicationContext.User;
}
iherwald commented 7 years ago

We had a similar problem when using CSLA in application extension modules of Autodesk products (e.g. AutoCAD or Revit). All assemblies are located in the same folder, but some dependent assemblies could not be loaded into the AppDomain of the host application when accessed in "server-side" methods of the local data portal.

Our solution is to handle the "AppDomain.CurrentDomain.AssemblyResolve" event by a custom "AssemblyResolver" to load the "missing" assemblies from the folder all assemblies are located in:

static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    if (!args.Name.StartsWith("Csla")) return null;
    //if (!args.Name.StartsWith("AnotherMissingAssembly")) return null;

    var parts = args.Name.Split(new string[] { "," }, 2, StringSplitOptions.None);
    var dllFileName = parts[0] + ".dll";

    Assembly resolvedAssembly = null;

    var targetFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), dllFileName);
    if (File.Exists(targetFile))
    {
        resolvedAssembly = Assembly.LoadFrom(targetFile);
    }

    // Return null when DLL is not found.
    // This will tell the .NetFramework to try other AssemblyResolve handlers.
    return resolvedAssembly;
}

static void StartExtensionApplication(string[] args)
{
    AppDomain.CurrentDomain.AssemblyResolve +=
        new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}

This post in a Autodesk forum pointed in the right direction: http://thebuildingcoder.typepad.com/blog/2014/05/rvtva3c-assembly-resolver.html This post in stackoverflow was helpful, too: http://stackoverflow.com/questions/1373100/how-to-add-folder-to-assembly-search-path-at-runtime-in-n...

michaelcsikos commented 7 years ago

Thanks for the reply. Turns out the ApplicationContextUserWorkaround doesn't always work, no big surprise. Every referenced DLL is sandboxed by the Office apps into separate folders at runtime with an __AssemblyInfo__.ini file. This makes it difficult to provide a path to Assembly.LoadFrom in the AssemblyResolve event handler. The path ends up something like

C:\Users\Michael\AppData\Local\assembly\dl3\JRAQQOT2.V1D\0DOV0CQ0.9XC\9da4093d\00226750_5600d301\Csla.DLL

michaelcsikos commented 7 years ago

CslaOffice.zip

I have attached a test solution which demonstrates some of the issues I am experiencing. It has a very basic custom identity, a basic business object which reads from a generated XML file, and a form with three buttons. It's using:

If you set the WindowsUI project as the StartUp project and run it, each button in the main form should succeed, with no exceptions being thrown.

Set the WordAddIn project as the StartUp project and run it. Word starts and the same form is displayed as the add-in loads. The first button succeeds, but the next two buttons throw exceptions. See the Debug Output for details, or set a breakpoint.

The same is true for the OutlookAddIn project.

As others have pointed out earlier in the thread, this is caused/related to .NET failing to resolve assemblies. The AssemblyResolve event is probably the answer, I just haven't been able to figure it out.

I am open to any suggestions. This is a commercial project I'm working on and this problem is currently a showstopper. Any help would be greatly appreciated.

Kind regards Michael

iherwald commented 7 years ago

After some investigations, I found a kind of work around. It works (all 3 test routines, sync and async) for me at the CslaOffice sample (WordAddIn with Office 2010):

    private void ThisAddIn_Startup(object sender, System.EventArgs e)
    {
        // SerializationWorkaround (handling the AppDomain.AssemblyResolve event ) does not help here.
        //SerializationWorkaround.Init();

        Csla.ApplicationContext.PropertyChangedMode = Csla.ApplicationContext.PropertyChangedModes.Windows;

        Csla.ApplicationContext.ContextManager = new Csla.Windows.ApplicationContextManager();

        // Work around for not receiving a 'SerializationException' in VSTO AddIn anymore:
        // 1. We have to force initialization of ConfigurationManager and
        System.Configuration.ConfigurationManager.GetSection("ANY DUMMY TEXT");
        // 2. We have to set UnauthenticatedPrincipal explicitly after setting the Csla.ApplicationContextManager.
        Csla.ApplicationContext.User = new Csla.Security.UnauthenticatedPrincipal();

        var form = new FormMain();

        form.Show();
    }

The reason for this behaviour in VSTO AddIns together with hints to the work around are discussed in two posts of the old CSLA Forum (it is really valuable that we have still access to it):

"VSTO and CSLA 2.0 Error" http://forums.lhotka.net/forums/t/1128.aspx?PageIndex=2

"Tests FAIL running in VS2012, but PASS in VS2010" http://forums.lhotka.net/forums/p/11545/55404.aspx

I hope this work around helps you out.

Kind regards Ingo

michaelcsikos commented 7 years ago

Ingo, wow, thank you! This problem has had me in a holding pattern for the last week; I've had to delay a new update.

Yesterday, I posted this question on Stack Overflow. If you have an account and would care to paste your answer there, I will mark it as the answer. https://stackoverflow.com/questions/45955078/c-sharp-serializationexception-in-office-2013-2016-vsto-add-ins-with-csla

Thanks again for your help.

michaelcsikos commented 7 years ago

CslaOffice 2.zip

I've come across another scenario where Ingo's workaround doesn't seem to work. It's simply a click event for a button in the ribbon:

private void MessageThrows(object sender, RibbonControlEventArgs e)
{
    MessageBox.Show("About to set user to UnauthenticatedPrincipal. " +
                    "Check Debug Output to see exception.",
                    "Before UnauthenticatedPrincipal");

    Csla.ApplicationContext.User = new Csla.Security.UnauthenticatedPrincipal();

    try
    {
        MessageBox.Show("The user has been set to UnauthenticatedPrincipal.",
                        "After UnauthenticatedPrincipal");
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex);
    }
}

The attached zip demonstrates the issue in Outlook and Word.

What I've found is that before any UI is displayed by any ribbon control event handler, I need to re-run the workaround code, and reassigning the current user is sufficient. It looks ridiculous, but it works.

/// <summary>
/// Microsoft Office add-ins can experience
/// <code>System.Runtime.Serialization.SerializationException</code> 
/// "Type is not resolved for member Csla.Security.UnauthenticatedPrincipal".
/// Calling this method before showing any UI works around the problem.
/// </summary>
public static void ExceptionWorkaround(bool setUnauthenticatedPrincipal = false)
{
    // 1. Force initialisation of ConfigurationManager

    ConfigurationManager.GetSection("Dummy");

    // 2. Set User explicitly

    if (setUnauthenticatedPrincipal)
        Csla.ApplicationContext.User = new UnauthenticatedPrincipal();
    else
        Csla.ApplicationContext.User = Csla.ApplicationContext.User;
}
iherwald commented 7 years ago

Good that it helped you.

In old CSLA forum, user "MikeGoatly" explained the work around in "Tests FAIL running in VS2012, but PASS in VS2010":

... When ConfigurationManager is initialized it reads from AppDomain.Evidence, which causes a stack walk - if you have placed a custom principal on the thread this ends up being de-serialized in the original AppDomain, which fails with the SerializationException you're seeing.

The fix is a little dirty, but if you make a call to ConfigurationManager.GetSection("ANY DUMMY TEXT") prior to your test then things should start working again. ...

michaelcsikos commented 7 years ago

There are still scenarios where all of these workarounds fail. I have loads of these exceptions logged on a client's PC. My only option seems to be installing CSLA in the Global Assembly Cache.

iherwald commented 7 years ago

A more resilient solution could be to rely on a neutral (build-in) type like GenericPrincipal/ClaimsPrincipal. These types are serializable. Unfortunately, at present they can not be serialized by CSLA MobileFormatter without beeing derived. There is some activity at the CslaClaimsPrincipal and serialization of ClaimsPrincipal subject: https://github.com/MarimerLLC/csla/issues/496 https://github.com/MarimerLLC/cslaforum/issues/422

However, in VSTO AddIns it seems to be better to avoid working with a custom (derived) Principal/Identity class at all.

A last resort could be to set the principal to a GenericPrincipal before the call from the VSTO AddIn AppDomain returns to the main AppDomain (MS-Office application). The challenge here is to find the correct entry/exit point in the VSTO AddIn. (Referring to the gist of a post in the old forum, proposed by Rocky)

michaelcsikos commented 7 years ago

By installing Csla.dll in the GAC it seems to solve the problem. No workarounds are required at all. It's a bit of a last resort, but so far it looks like the only reliable solution.