Friday, November 27, 2009

Reflection, byref and _COM

When using Refection against .NET codebyref just works as you would expect it to. When you are reflecting against COM byref does not work as you may expect.


If the target is .NET the following code would just work. The fact that we are creating the object from a class ID tells us it’s COM, so it won’t work.


var addin = GetCOMObject();

object[] objparams = new object[] { intVal,stringVal,etc};


var addintype = Type.GetTypeFromCLSID(new Guid("ADADAF30-E012-45db-95BE-E7544E918EBD")); // An Outlook Addin


var rval = addintype.InvokeMember(methodname, BindingFlags.InvokeMethod, null,addin, objparams);


Assume the method that we are invoking passes something back byref to param 2. The following code will not work show you the value :


stringval = (string) objparams[1];


To make this work with COM, you need to use ParameterModifers.


So, your code will look like


var addin = = GetCOMObject();

object[] objparams = new object[] { intVal,stringVal,etc};

ParameterModifier mods = new ParameterModifier(_params.Length);

mods[1] = true; // param 2 passes back by ref

ParameterModifier[] modstopass = { mods }; // we need an array
var addintype = Type.GetTypeFromCLSID(new Guid("ADADAF30-E012-45db-95BE-E7544E918EBD"));


var rval = addintype.InvokeMember(methodname, BindingFlags.InvokeMethod, null, addin, objparams, modstopass, null, null);


We had this issue with a C# Add-in for Outlook.


Let’s hope this “just works” with C#4 and the dynamic type. ..

Tuesday, November 24, 2009

Errors Enumerating DataRows in a DataTable in a strongly typed DataSet that has been upgraded from .NET 2.0 to .NET 3.5

In Visual Studio 2008 the version number of MSDataSetGenerator, the tool which generates strongly typed DataSets based upon a *.xsd file, increased from 2.0.50727.1433 to 2.0.50727.3074. The only significant difference in the output files is that when targeting framework .NET 3.5, the new version of the tool uses System.Data.TypedTableBase (from System.Data.DataSetExtensions) as the base class rather than System.Data.DataTable.
This sounds like a small difference but it can trip you up! Let’s say I have the following program:
class Program
{
   static void Main(string[] args)
   {
      SearchResultsDataSet ds = new SearchResultsDataSet();
      ds.SearchResults.AddSearchResultsRow(Guid.NewGuid(), "Ref1", "Title1");
      ds.SearchResults.AddSearchResultsRow(Guid.NewGuid(), "Ref2", "Title2");
      ds.AcceptChanges();
      foreach (SearchResultsDataSet.SearchResultsRow row in ds.SearchResults)
      {
         Console.WriteLine(row.Ref);
      }
      Console.ReadLine();
   }
}

SearchResultsDataSet is a strongly typed DataSet in a separate assembly called X. Assembly X and my test program are both built targeting .NET 2.0, and deployed to a customer site. I subsequently upgrade both my test program and X to .NET 3.5 and rebuild them, but only deploy the test program to the customer site; X has not changed, so I leave the existing copy in place. When my test program attempts to enumerate the DataRows in the “SearchResults” table I will get the following error:

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug> TypedDataSetTest.exe

Unhandled Exception: System.MissingMethodException: Method not found: 'System.Collections.IEnumerator SearchResultsDataTable.GetEnumerator()'.
at TypedDataSetTest.Program.Main(String[] args)

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>

Or let’s say I do this the other way around: I upgrade both projects to .NET 3.5, rebuild both but deploy the new version of X alongside the old version of the test program. When I try to enumerate the “SearchResults” table I will get the following error:

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>
TypedDataSetTest.exe

Unhandled Exception: System.NullReferenceException: Object reference not set to
an instance of an object.
at System.Data.TypedTableBase`1.GetEnumerator()
at TypedDataSetTest.Program.Main(String[] args) in C:\Users\alexr\Documents\V
isual Studio 2008\Projects\TypedDataSetTest\Program.cs:line 17

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>

What’s really going on AND what can make this problem difficult to spot
You may be thinking “What kind of fool would change the .NET framework an assembly was targeting and then not deploy it to the live environment”. And you’re right. But consider the following scenario:

  1. Create a strongly typed DataSet in an assembly that targets .NET 2.0.
  2. MSDataSetGenerator generates a .Designer.cs file containing the code.
  3. Upgrade the assembly to target .NET 3.5.
  4. The .Designer.cs file is already in place and it doesn’t get regenerated. It doesn’t get regenerated UNTIL you make a change to the *.xsd file.
This caught us out with our product, because we deploy Hotfixes by keeping the Assembly Version Number the same and deploying only assemblies which have been modified (using other file metadata to distinguish them). We were caught unawares by our .NET 3.5 strongly typed DataSets suddenly morphing from being based upon System.Data.DataTable to being based upon System.Data.TypedTableBase and some of our integration components broke.

Another thing that makes this problem hard to identify is that a lot of us (quite rightly) try to diagnose problems like this by using Reflector to decompile the assembly. In this case though, the source code of the test program is the same in both cases; it’s the underlying IL that is different. The convenient C# construct of foreach (SearchResultsDataSet.SearchResultsRow row in ds.SearchResults) has this going on behind the scenes:

First, for the .NET 2.0 version:

L_0046: callvirt instance class [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet::get_SearchResults()
L_004b: callvirt instance class [mscorlib]System.Collections.IEnumerator [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable::GetEnumerator()
L_0050: stloc.2
L_0051: br.s L_006d
L_0053: ldloc.2
L_0054: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
L_0059: castclass [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsRow
L_005e: stloc.1

And then the .NET 3.5 version:

L_0046: callvirt instance class [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet::get_SearchResults()
L_004b: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 [System.Data.DataSetExtensions]System.Data.TypedTableBase`1::GetEnumerator()
L_0050: stloc.2
L_0051: br.s L_0068
L_0053: ldloc.2
L_0054: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1::get_Current()


(I might have missed a bit off the start and the end, I don’t understand IL that well). As you can see, in the second example the compiled assembly uses a generic enumerator rather than a standard System.Collections.IEnumerator. But this difference is not visible in C#.

Friday, November 20, 2009

Solving the Double Hop Issue

With a Batch File

I have come across many blogs and websites that go into great detail on how to solve the double hop issue. I have shared these with my colleagues and customers they all think that they seem too complex, and are therefore do not want to use Kerberos.

Solving the Double Hop Issue with Kerberos is not complex.
This diagram illustrates the problem.



This is taken from a very good blog on the same subject which goes into way more detail…

What you need to fix it

  • I would recommend running services as a DOMAIN USERs rather than network service or local users.
  • (“services” meaning Application Pools for web sites and SQL services)
  • Service Principal Names.
  • Local Machine Rights granted to the Service Accounts.
    • Act as part of operating system
    • Impersonate user
  • Settings in AD for the Machines to allow delegation.
  • Settings in AD for the Service Accounts to allow delegation.

 

What does this mean

A Service Principal Name (SPN) is the name by which a client uniquely identifies an instance of a service. For example HTTP/Servername is the SPN for a web site at Servername. MSSQLSvc/ServerName:1234 is the SPN for a SQL Service running on Servername on Port 1234 (see http://msdn.microsoft.com/en-us/library/ms677949(VS.85).aspx for more information on service principal names).

Local Machine rights are managed with Local Security Policy (found under Administrative Tools) .

Delegation is to pass someone’s user credentials to another process or server.

Both AD settings are edited with the MMC Snap-in “Active Directory Users and Computers”. You need to allow both the service users and the machine to delegate.

Some tools from the web


You need :
  • Found on the windows resource kit
    • NTRIGHTS.EXE (Set local machine rights)
    • SETSPN.exe
  • Installed with IIS
    • adsutil.vbs
  • On codeplex
    • machinedelegation.vbs
    • userdelegation.vbs
  • Below and on codeplex
    • Enable.bat

The Batch File


@REM ==================================
@REM Setup VARS
@REM ==================================

set MACHINE1=DBLHOP
set MACHINE2=DBLHOPSVR

set FQDN=domain.local

set USERACCOUNT=DOMAIN\ServiceAccount
@REM WEBID is the WEBSITE ID, THE Default website is 1
set WEBID=1

@REM ==================================
@REM Use adsutil to config IIS to use kerberos
@REM ==================================

cscript C:\inetput\adminscripts\adsutil.vbs set w3svc/WebSite/root/NTAuthenticationProviders Negotiate

@REM ==================================
@REM Use SETSPN to create the SPNs
@REM ==================================

setspn -A HTTP/%MACHINE1% %USERACCOUNT%
setspn -A HTTP/%MACHINE1%.%FQDN% %USERACCOUNT%

setspn -A HTTP/%MACHINE2% %USERACCOUNT%
setspn -A HTTP/%MACHINE2%.%FQDN% %USERACCOUNT%

@REM ==================================
@REM Use NTRIGHTS to enable Act as part of operating system and
@REM Impersonate a client after authentication
@REM ==================================

ntrights +r SeTcbPrivilege -u %USERACCOUNT% -m \\%MACHINE1%
ntrights +r SeTcbPrivilege -u %USERACCOUNT% -m \\%MACHINE2%

ntrights +r SeImpersonatePrivilege -u %USERACCOUNT% -m \\%MACHINE1%
ntrights +r SeImpersonatePrivilege -u %USERACCOUNT% -m \\%MACHINE2%


@REM ==================================
@REM Use VB Scripts to give the user the delegate right
@REM ==================================

cscript userdelegation.vbs %USERACCOUNT% enable

@REM ==================================
@REM Use VB Scripts to give the machine the delegate right
@REM ==================================

cscript machinedelegation.vbs %MACHINE1%
cscript machinedelegation.vbs %MACHINE2%

@REM ==================================
@REM Use IISRESET to restart IIS on both servers
@REM ==================================

iisreset %MACHINE1%
iisreset %MACHINE2%

Really good external websites

blog foo
ASP.NET Applicaiton to help

More Info

Diagonal

Monday, November 9, 2009

Wisdom and Microsoft Dynamics CRM

The wisdom team have been busy integrating Wisdom into Microsoft Dynamics CRM. Cases created in CRM can now be linked to a Wisdom case area. This gives Dynamics CRM Document and Records management capabilities and much more.

We believe that we have taken an interesting approach to this problem that will allow CRM administrators to link any CRM entity type to Wisdom thus giving any CRM entity EDRM capabilities.
Useful links

Microsoft Dynamics http://www.microsoft.com/dynamics/
Wisdom http://www.diagonal-consulting.com/Technology/Wisdom/Pages/Default.aspx