Monday, 15 December 2008

Unable to determine the identity of domain

Don't you just hate it when you have some code that works fine in your development environment but not when you hand it to testers?

Loyal readers might recall that I have been migrating a bunch of legacy Win32 code to .Net. In the case of our GUI editor, that people use to map out their business processes, I have been writing a C# COM component that gets called from the legacy Delphi.Win32 code. Gradually more and more functionality is migrating to C#.

Along the way we have been storing the user's business process map in an OpenXML package. This is basically a ZIP file containing images, the model XML and bits of XML specifying how things are connected. Then it's a case of using System.IO.Packaging to get at it all.

Testing discovered that some larger client models were failing to upgrade with the mysterious error message "Unable to determine the identity of domain". This turns out to have nothing to do with Windows domains or user permissions.

As with all things .Net, someone somewhere has no doubt hit this problem before. Kevin Rohrbaugh had a similar scenario
http://www.coderoni.com/2008/04/04/server-side-office-document-generation-bug/
. His workaround was to ensure that they were running under an account that did not have a profile on the local machine (more below). That's not an option for us.

I'll spare you a gruesome recap of the full debugging which involves much "spelunking" with Reflector as Kevin so aptly calls it. Here are the broad strokes:

When you get a stream on a part in an OpenXML package (so that you can get at the uncompressed bits), you think you are getting something in-memory. However, if the package part is too big (more than 1.3Mb compressed), the framework decides to unzip the entire package part to disk and to give you a handle on that instead. This has unforeseen performance consequences, but we'll ignore them for now.

Where does it unzip them to? Isolated Storage. Exactly what kind of isolated storage depends on the user account you are running under. If you are running under a user account that doesn't have a profile on the local machine, the framework uses a machine-scoped location. If you are running under an account that does have a profile on the local machine (as we will be -- it'll be running under the account of the user of the GUI tool) it uses a user-scoped account.

If I add a reference to the assembly containing the code that accesses the package, and run it in the debugger all is well. But under COM all is far from well.

Running reflector on the Isolated Package class shows that when using user-scoped isolated storage, the framework examines the "evidence" of the AppDomain (where an AppDomain is a lightweight process). Under COM, we are running in a DefaultDomain that doesn't have any evidence.

You can't set the evidence for an AppDomain once it has been started and it's not possible to specify that the COM DLL should run with certain evidence or in a special AppDomain. Running the "Microsoft .NET Framework 2.0 Configuration" tool and granting Full Trust to our assembly doesn't solve the problem because there is still no "evidence" for the Framework code to examine. So there are two options:

1) In the Win32 code, host the CLR, create an AppDomain with the appropriate evidence and load the assembly. Use reflection to get at the methods.

2) (What I did in the interests of expediency). In the COM component, create a new AppDomain with the appropriate evidence. and execute the code in that. This works fine. There is a performance hit because we are now marshaling across AppDomains as well as marshaling across COM. We will see if the performance is acceptable. If not we will have to go with (1).

Doing (2) is similar to what you have to do for Office add-ins. For Office add-ins, the recommended strategy to satisfy the security model is to have an unmanaged shim. You sign the shim to make Office happy. Office talks to the shim, the shim acts as a proxy passing everything to your managed code.

In our case, the COM interface now loads up the AppDomain and proxies calls to an instance of our class running in that AppDomain.

The crucial (necessary and sufficient) piece of evidence is that we require the code to be running in the MyComputer zone.

First we need a simple AppDomainSetup:

AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory.ToString();


Then we need our evidence

Evidence evidence = new Evidence();
evidence.AddHost(new Zone(SecurityZone.MyComputer));


Now we can fire up an AppDomain running with that evidence.

AppDomain hostedAppDomain = AppDomain.CreateDomain("Demo", evidence, setup);


Now we get a handle on an instance of our class running in that AppDomain

ObjectHandle handle = hostedAppDomain.CreateInstance("MyStuff.Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d0e8b069449d61a1", "MyStuff.Demo.DemoComponent");


To pull this off, DemoComponent has to inherit from MarshalByRefObject so now we have a little .Net remoting magic to do. We have to get a lease on the object and extend its lease if we are not done with it. A trivial class LicenseRenewer that implements ISponsor does the trick

lease = (ILease)handle.GetLifetimeService();
lease.Register(leaseRenewer);


Finally we can get a usable instance of the class. We access this locally and it transparently proxies calls to the other AppDomain.

demoComponent = (IDemoComponent)handle.Unwrap();


Now calls to our COM interface can just explicitly proxy to demoComponent e.g.

public bool Demo()
{
return demoComponent.Demo();
}


Then our COM interface just proxies things to demoComponent. Any types you want to marshall across AppDomains have to marked [Serializable()] of course.

9 comments:

Aymeric Nguyen said...

Thank you ! You saved my day :)

Richard said...

Thanx you just saved another persons day!

found many solutions to this problem, none worked for me.

The missing part was the remoting.

thanx and keep up the good work

harsha said...

no luck with this

i am try to use openxml to save the large xlsx file in SSIS.
its throwing :

Error Details:Unable to determine the identity of domain.
here is my code :

private static void
ExportToExcel1(System.Data.DataSet dsFilterFinal, string path)
{
writeLog("start of export to excel", DateTime.Now);
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = "C:\\";
setup.DisallowBindingRedirects = false;
setup.DisallowCodeDownload = false;

setup.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
evidence.AddAssembly(typeof(ExcelUsingOpenXML.ExcelUtils).Assembly);
evidence.AddHost(new Zone(SecurityZone.MyComputer));
//Evidence evidence = new Evidence();
//evidence.AddHost(new Zone(SecurityZone.MyComputer));

AppDomain ad = AppDomain.CreateDomain("NewAppDomain", evidence, setup);
ObjectHandle handle = ad.CreateInstance(typeof(ExcelUsingOpenXML.ExcelUtils).Assembly.FullName, typeof(ExcelUsingOpenXML.ExcelUtils).FullName);
handle.GetLifetimeService();
var lease = (ILease)handle.GetLifetimeService();
MyClientSponsor sponsor = new MyClientSponsor();
lease.Register(sponsor);



// lease.Register();

//ExcelUsingOpenXML.ExcelUtils excelUtils = (ExcelUsingOpenXML.ExcelUtils)ad.CreateInstanceAndUnwrap(typeof(ExcelUsingOpenXML.ExcelUtils).Assembly.FullName, typeof(ExcelUsingOpenXML.ExcelUtils).FullName);

//ReadAndWriteScript.ExcelUtils excelUtils = (ReadAndWriteScript.ExcelUtils)ad.CreateInstanceAndUnwrap(typeof(ReadAndWriteScript.ExcelUtils).Assembly.FullName, typeof(ReadAndWriteScript.ExcelUtils).FullName);
ExcelUsingOpenXML.ExcelUtils excelUtils = (ExcelUsingOpenXML.ExcelUtils)handle.Unwrap();
excelUtils.SaveWorkbook(path, dsFilterFinal, false, false);

AppDomain.Unload(ad);

Anonymous said...

Hi


The piece I'm missing is this :

"A trivial class LicenseRenewer that implements ISponsor does the trick
"


What exactly is it that this class is doing?

Tim Lewis said...

I found it to be less cumbersome to muck with the internals of the current AppDomain rather than spinning up a new one and Marshaling the data back and forth.

[code]
using System;

namespace AppDomainEvidence
{
class Program
{
static void Main(string[] args)
{
var initialAppDomainEvidence = System.Threading.Thread.GetDomain().Evidence; // setting a breakpoint here will let you inspect the current app domain evidence
try
{
var usfdAttempt1 = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForDomain(); // this will fail when the current AppDomain Evidence is instantiated via COM or in PowerShell
}
catch (Exception e)
{
// set breakpoint here to inspect Exception "e"
}

// Create a new Evidence that include the MyComputer zone
var replacementEvidence = new System.Security.Policy.Evidence();
replacementEvidence.AddHostEvidence(new System.Security.Policy.Zone(System.Security.SecurityZone.MyComputer));

// Replace the current AppDomain's evidence using reflection
var currentAppDomain = System.Threading.Thread.GetDomain();
var securityIdentityField = currentAppDomain.GetType().GetField("_SecurityIdentity", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
securityIdentityField.SetValue(currentAppDomain,replacementEvidence);

var latestAppDomainEvidence = System.Threading.Thread.GetDomain().Evidence; // setting a breakpoint here will let you inspect the current app domain evidence

var usfdAttempt2 = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForDomain(); // this should
}
}
}
[/code]

Anonymous said...

Thank you for your sharing this code. It was extremely helpful and educational.

After struggling with this issue for a few days, I found that the fix was in downloading OpenXML SDK from Microsoft:
git clone https://github.com/OfficeDev/Open-XML-SDK OpenXML
download SpreadsheetLight source code from here:
http://spreadsheetlight.com/developers/
Removing reference from WindowsBase.dll assembly in the project.
And small fix in CloseAndCleanUp method due to relStream object handling.

Unknown said...
This comment has been removed by the author.
Unknown said...

Hi .
my use open xml sdk 2.0
.net framework version:3.5

i want export excel

but my source code is IsolatedStorage error.


public void Macro_excelExport()
{





//System.AppDomain newDomain = System.AppDomain.CreateDomain("NewApplicationDomain");

// Load and execute an assembly:
//newDomain.ExecuteAssembly(@"c:\HelloWorld.exe");

// Unload the application domain:

System.AppDomain newDomain = System.AppDomain.CreateDomain("NewApplicationDomain");

// Load and execute an assembly:
newDomain.ExecuteAssembly(System.Windows.Forms.Application.ExecutablePath);

// Unload the application domain:

DateTime startTime = DateTime.Now;

DataTable sourceTable1 = GetSampleTable2();


DataSet sourceSet = new DataSet();

//ExcelHelper asd = new ExcelHelper();

sourceSet.Tables.Add(sourceTable1);
//sourceSet.Tables.Add(sourceTable2);
bool pan = ExcelHelper.CreateExcelDocument(sourceSet, "c:\\Temp\\result.xlsx", 1, 1);


DateTime endTime = DateTime.Now;

MessageBox.Show("end time : " + (endTime - startTime).ToString() + "result:" + pan.ToString());
//System.AppDomain.Unload(newDomain);
//workerThread.Join();
System.AppDomain.Unload(newDomain);
}



public DataTable GetSampleTable2()
{

DataTable table = new DataTable();

table.Columns.Add("no1", typeof(string));
table.Columns.Add("no2", typeof(string));
table.Columns.Add("no3", typeof(string));
table.Columns.Add("no4", typeof(string));
table.Columns.Add("no5", typeof(string));

for (int i = 1; i <= 180000; i++)
{
table.Rows.Add("A" + i.ToString(), "B" + i.ToString(), "C" + i.ToString(), "D" + i.ToString(), "E" + i.ToString());
}

table.AcceptChanges();

return table;
}

Anonymous said...

2020 here - and this is still a life saver. Ho do we have caps to the tune of 1mb in 2020???
Thank you. Thank you. Thank you. Duct taped Tim Lewis's version into our code and it just worked. Wow.