Friday, August 3, 2007

Be careful what you name your .exe ...

Windows Vista has some annoying heuristics that look at executables to decide whether they need administrator privileges or not. Unfortunately, these heuristics are fairly brain-damaged.

It seems that putting the word "install" or "update" anywhere in the file name or version properties will trigger the "require administrator elevation" feature in Vista. I had a program that had the word "Updater" in it, which of course frustrated me for a long time as to why Vista wouldn't let me run it as a normal user. I changed "Updater" to "Launcher" and it fixed everything right up.

Oh, and as a side note, don't use the Process class on Windows Vista or Windows Server 2003. Process uses the perf counter to do a lot of its work, which means a normal user can't use it.

Friday, July 20, 2007

Finding the interfaces you want

This is a simple little .NET reflection trick, but one which not a lot of people realize how to do (I get asked about this a lot). Suppose you want to search an assembly for all the types that implement a given interface. For example, your application supports plug-ins and you want to load a third-party assembly that potentially contains plug-in classes that implement your plug-in interface.

C#:

Assembly assembly = Assembly.LoadFile(@"C:\path\to\3rdparty\plug-in.dll");
foreach (Type type in assembly.GetTypes())
{
if (typeof(IMyPlugin).IsAssignableFrom(type))
{
IMyPlugin plugin = (IMyPlugin)assembly.CreateInstance(type.FullName);
// do something with the plugin ...
}
}


VB.NET:

Dim assembly As Assembly = Assembly.LoadFile("C:\path\to\3rdparty\plug-in.dll");
Dim type As Type
For Each type In assembly.GetTypes()
If GetType(IMyPlugin).IsAssignableFrom(type)
Dim plugin As IMyPlugin = CType(assembly.CreateInstance(type.FullName), IMyPlugin)
' do something with the plugin ...
End If
Next


It's pretty straightforward. First, we use the Assembly object to load the third party assembly from its DLL. Then, we use the Assembly.GetTypes() method to iterate through all of the classes present in the assembly. Inside the loop, we use Type.IsAssignableFrom() to determine whether the class can be assigned to our IMyPlugin interface. Finally, we create an instance of the class and assign it to a variable of type IMyPlugin so that we can actually do something with it (note that the above code assumes the class has a parameterless constructor).

Note that you want to check the possibilty of assignment in the order shown above, i.e. typeof(interface).IsAssignableFrom(class). If you do it the other way around, it won't work. Basically this statement is testing to see if this is possible:

C#:

interface variableName = new class();


VB.NET:

Dim variableName As interface = New class()

Friday, June 29, 2007

ADO.NET 101

I'll file this under the "stupid mistakes" category, but I'll admit to it here just in case someone else didn't realize this until it was too late like I did.

When you have a SqlDataReader open, you can't reuse the underlying SqlConnection. In my case, I just changed to using a DataSet instead of a reader (I don't remember why I used a reader in the first place... the query always returned 1 row). Another solution would be to open a second connection.

This support document puts it quite succinctly:

While the SqlDataReader object is in use, the associated SqlConnection object serves the SqlDataReader, and you cannot perform any other operations on the SqlConnection object other than to close it. This is true until you call the Close method of the SqlDataReader object.

Friday, June 22, 2007

Annoying Xcode problem

Taking a bit of a jaunt off into left field from my usual .NET posts...

I was trying to create a Java project under Xcode in Mac OS X 10.4. Strangely, right out of the box the template application project compiled but didn't run. I kept getting a java.lang.UnsupportedClassVersionError.

Seems that after Java 1.5 came to the OS X platform, it gets selected as the default JDK for compilation, but Xcode thinks it's still working with a 1.4 target so it creates a script that starts the app using 1.4.

Once you realize that, it's easy enough to change the settings in the project to fix it. This Apple Q&A explains it in detail.

Thursday, June 21, 2007

References and classes... make sure you mean it

All too often, I see things like this:

C#:
public void DoSomethingToAClass(ref AClass objectToChange)
{
// ... change some member variables of objectToChange or whatever
}


VB.NET:
Public Sub DoSomethingToAClass(ByRef objectToChange As AClass)
' ... change some member variables of objectToChange or whatever
End Sub


This is totally redundant. Classes are reference types. Therefore, by default they are passed by reference. This means that you can drop the ref or ByRef and achieve the same results. What's worse is that in the examples above, you're creating a reference to a reference, which just creates extra work for the application dereferencing them twice to find the actual object.

On the other hand, if what you're passing in to the method is a value type, then this is what you want. Your intrinsic types (such as Int32) and struct (C#) or Structure (VB.NET) are all value types. These need the ref or ByRef tag, otherwise your changes will be lost when the method exits. Note that while Arrays and Strings are also technically reference types, the framework handles them somewhat differently, and you must use ref or ByRef with them to get the desired result.

There are times when a reference to a reference is what you want, especially with certain P/Invoke methods that ask for a "pointer to a pointer" as one of the parameters. In this case, using ref or ByRef with a reference type makes sense and is what you want.

Monday, May 14, 2007

Visual Studio .NET 2003, ASP.NET v1.1, and Windows Vista

I recently needed to make a .NET Framework v1.1 application work on Windows Vista, which meant debugging it and running the development environment on a Windows Vista Business machine.

While quite possible, it did give me some headaches.

Enable the IIS 7 Product Feature

Obviously, if you need to work with ASP.NET applications, you need IIS. Windows Vista comes with IIS 7, which is vastly different from the IIS 6 architecture.

You can install IIS 7 from the "Programs and Features" control panel, under "Turn Windows features on or off." I recommend choosing all the items under "Internet Information Services" : "Web Management Tools" : "IIS 6 Management Compatibility" when you install the rest of IIS.

Install Microsoft .NET Framework v1.1

Windows Vista does not come with v1.1 of the framework pre-installed. Instead, you need to download and install these packages:


When installing, be sure to right-click on the install package and select "Run as administrator."

After you're done installing these packages, run the command prompt as administrator, and then CD to C:\Windows\Microsoft.NET\Framework\v1.1.4322. Run the following command:


aspnet_regiis -ir -enable


Install Visual Studio .NET 2003

This is the easy part. Visual Studio .NET 2003 will work on Windows Vista, but it's a bit quirky. First off, the installer won't be able to install all of the prerequisites (specifically FrontPage Extensions). I was able to skip that part and install just fine, however. Make sure when you install, you right-click on the installer package and select "Run as administrator." Install Visual Studio .NET 2003 Service Pack 1 as well.

After it's installed, Visual Studio .NET 2003 will fail to run in its default state. Unfortunately, you have to run it with elevated privileges. In order to do this every time without right-clicking and selecting "Run as administrator," follow these steps.
  • Right-click on the "Microsoft Visual Studio .NET 2003" shortcut and select "Properties."
  • On the "Shortcut" tab, click "Advanced..."
  • Check the box that says "Run as administrator."
  • Click "OK" until you're out of the shortcut properties.


Change IIS Settings to Accommodate ASP.NET v1.1 Applications

This is the part I got hung up on. I kept getting strange errors when trying to open web applications under Microsoft Visual Studio .NET 2003, mostly it thinking that the virtual directory was a v1.0 application. To enable ASP.NET v1.1, you need to do the following:
  • Open Internet Information Services (IIS) Manager.
  • Highlight your local server. In the Features View, locate the IIS section and double-click "ISAPI and CGI Restrictions."
  • Right-click item "ASP.NET v1.1.4322" and click "Allow."
  • Highlight the virtual directory for your ASP.NET v1.1 application or the entire web site it's under.
  • Right-click on the highlighted item and choose "Advanced Settings..."
  • Under "Behavior", change the item "Application Pool" from "DefaultAppPool" to "ASP.NET 1.1."


At this point, you should be good to go.

Tuesday, April 10, 2007

Using .NET objects via COM, part 2

As an addendum to my previous post, I just struggled through troubleshooting a couple of problems with using the .NET COM interop with regasm.

All members of your exposed classes must only reference other exposed classes. For instance, I added an internal class to my assembly and then created a field that stored an instance of it in my public object that was to be exposed during COM. This resulted in another obscure error code in Server.CreateObject: 8013150a.

Note that this only applies to instances of classes exposed through properties or fields in your exposed object, even if those fields/properties are private, protected, or internal. Objects instantiated inside methods and stored as local variables do not appear to cause any problems.

Debug.WriteLine will cause NullReferenceException to be thrown when a debugger is not attached. This one drove me nuts for a while; I kept getting a NullReferenceException in an odd place that didn't make any sense when I tried to call one of my methods from COM (it worked just fine calling it from .NET). I attached WinDbg to the w3wp.exe process to trace what was going on, and suddenly it stopped throwing the NullReferenceException. I unattached the debugger, and it came right back!

I pondered a moment what difference the debugger would be making, and on a whim I went through my object and removed any references to the Debug class. Apparently that did the trick, as now I have no more problems with a mysterious NullReferenceException being thrown.

Monday, April 9, 2007

Using .NET objects via COM

Recently I created an object in .NET that I needed to use both from ASP.NET applications and classic ASP. I really didn't want to create a web service interface for just this one little object, so I delved into the wonderful world of COM again.

If you want to instantiate your .NET object via COM, there are some basic rules you have to follow.
  • The object must implement a public, parameterless constructor.
  • Only your instance methods and properties will be visible via COM (i.e. no static stuff).
  • You have to take care when using overloaded methods.

First, let's look at the code in C#.

using System;
using System.Runtime.InteropServices; // for the custom attributes below

namespace MyCompany
{
[ComVisible(true)]
[Guid("4663D268-454F-4338-B6BB-44537F5A0266")]
[ProgId("MyCompany.MyProject.MyClass")]
public class MyClass
{
private bool boolfield;

public MyClass()
{
boolfield = false;
}

public bool BoolField
{ get { return boolfield; } set { boolfield = value; } }

public string PrintBoolField()
{
return "BoolField is currently " + BoolField;
}

public string PrintBoolField(bool newValue)
{
BoolField = newValue;
return PrintBoolField();
}
}
}


There's several things going on in the above example. First of all, it's a pretty simple class. We have an overloaded set of methods, one property, and one constructor. You'll notice it's decorated with three attributes:

ComVisibleAttribute: Necessary so that your class will be exported to COM. You can also set this one at the assembly level.

GuidAttribute: This allows you to explicitly set a GUID for the generated COM object. If you don't specify one, the steps below will create one automatically, but it's best if you keep a consistent GUID in each installation when working with the same object, since you never know who will try to reference you by GUID.

ProgIdAttribute: This is only necessary if you want to override the ProgId of your COM object. By default, the ProgId will be <namespace>.<classname>.

So once you've got that worked out, compile your object into a class library. The next thing you need to is register your assembly with COM.

In your .NET framework folder you'll find a handy tool called regasm. regasm lets you register .NET assemblies as COM objects and does all the magic behind-the-scenes work for you.

There are two ways you can set this up:
  • Put the assembly in the global assembly cache (GAC), and then register with regasm. This is good if you want your COM object to be globally accessible.
  • Put the assembly in the application folder of the client application you want to be able to call the object. Register with regasm using the /codebase flag. The COM object will only be accessible to that application.


The choice is up to you based on your particular needs, but I recommend using the GAC. This is because you might want to use the object again elsewhere, and it's a pain in the butt to unregister and then re-register COM objects due to locking issues.

Now that you've done that, you can create an instance of your COM object from classic ASP like normal:


<html>
<body>
<p>
<%
Dim myobj : Set myobj = Server.CreateObject("MyCompany.MyProject.MyClass")

myobj.BoolField = True
' Should print: BoolField is currently True
Response.Write myobj.PrintBoolField()
Response.Write "<br>"

' Should print: BoolField is currently False
Response.Write myobj.PrintBoolField_2(False)
%>
</p>
</body>
</html>


And it's that easy. You can now access your nice managed .NET object in classic ASP/VBScript as if it were a COM object. And, of course, this is not limited to ASP... you can do this anywhere that you would normally be able to instantiate COM objects. Although, make sure you remember that your COM object is actually .NET... it would be worrisome to forget and sometime down the road import it into a .NET project with a redundant Interop assembly.

You may notice in the example code above, the overload to PrintBoolField has been renamed to PrintBoolField_2. That didn't exist in the original code, did it? You're right, it didn't.

COM doesn't support method overloads. When regasm builds its COM magic, it automatically appends a suffix of _2, _3, etc. to any overloaded methods. If you get confused about which overload is which, you can use a tool like OLE View® to help you figure it out.

One last note: Make sure that any base classes or interfaces your objects implement are also COM visible as well. Otherwise, you will get a mysterious error code 80131509 whenever you try to create your object.

Monday, April 2, 2007

Close()? Dispose()? A .NET programmer needs not these things!

Hogwash.

In reading Tess' MSDN blog this morning, it got me thinking about some things with object disposal and garbage collection. Question 12 on her little pop quiz in particular:

12. Why is it important to close database connections and dispose of objects? Doesn't the GC take care of that for me?


Of course, the first poster already answers it:

12/ It does, in the finalizer thread, which takes a _long_ time to process things. Basically this shove stuff like connection pooling out of the window, not to mention that basically resources are held for much longer.


I've witnessed several occasions where the Close() and Dispose() methods are completely ignored by programmers. Streams are left hanging, web services are left dangling... it's a mess!

Usually Close() isn't so much of a problem. Developers are typically pretty aware when they're opening a file or database connection. Still, you need to be keenly aware of what you're doing when you write something such as this:


public class SqlConnectionMgr
{
public SqlConnection GetDBConnection(string connectionString)
{
return new SqlConnection(connectionString);
}
}


This is overly simplified, but I've seen similar code dozens of times. Now you've just create a connection to the database, but you've thrown it out into the wild to hope that whomever calls your code remembers to close it. If you can trust that person, fine. If not, you may find it better do something like this:


public class SqlConnectionMgr : IDisposable
{
private SqlConnection connection;

public SqlConnection GetDBConnection(string connectionString)
{
if (connection != null && connection.State == ConnectionState.Open)
connection.Close();
connection = new SqlConnection(connectionString);
return connection;
}

public void Dispose()
{
if (connection != null && connection.State == ConnectionState.Open)
connection.Close();
GC.SuppressFinalize(this);
}

~SqlConnectionMgr()
{
Dispose();
}
}


Now you can retain management of the connection in your own class. You have to implement IDisposable now, but that's a more implicit direction to the consumer that something has to be done, and you can take part of the onus of managing resources off the consumer in case they forget.

Some things to remember when implementing IDisposable:
  • Make sure to also implement a finalizer which calls your Dispose() method in case the programmer forgets to dispose of the object.
  • In the Dispose() method, you need to call the garbage collector's SuppressFinalize(object) method. This will ensure that if the object was already disposed, the finalizer is not called needlessly.


(And yes, I know that in changing my example above it no longer is able to pass out multiple connections. I'll leave that as a challenge for the reader.)

IDisposable is an excellent way of managing resource, because, well, that's why it was added to the framework. You need to be keenly aware of whether or not the objects you're consuming implement this interface. (The easiest way to find out if you're in the middle of coding is to look through the object's IntelliSense list for the Dispose() method.)

C# provides an excellent mechanism for working with disposable objects known as the using statement:


using (MyDisposableObject obj = new MyDisposableObject())
{
// do something ...
}


When this code executes, the object is created and neatly disposed of within the scope of the using statement. This basically is just a clean, shorthand way of writing:


MyDisposableObject obj = new MyDisposableObject();
// do something ...
obj.Dispose();


There are a lot of objects in the .NET framework that use resources that need to be disposed, including unmanaged resources that can wreak havoc if left open waiting for the garbage collector to come along. One in particular that I have run into is the Bitmap class. If you don't dispose of a Bitmap when you're done with it, depending on the size of the image you could end up with a huge chunk of the heap sitting around doing nothing, waiting to be removed from memory at some indeterminate point in the future.

While .NET provides a lovely managed platform, be sure that you are still keenly aware of resources that you're using. While it's not as easy to lose pointers and form memory leaks in a .NET application as it is in an unmanaged C++ application, you can still waste plenty of system resources waiting around for the garbage collector to do your cleanup for you.

Thursday, March 29, 2007

How to remove empty elements from an XML document

Since this is kind of universally useful, I recently was able to perfect a little snippet of XSL to remove empty elements from an XML document. This only removes elements that have no inner content or attributes. (In case you're wondering, I did this to handle special legacy XML documents for serialization.)


<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="no" indent="no"/>
<xsl:strip-space elements="*" />
<xsl:template match="*[not(node()) and not(./@*)]"/>
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>


And in case you're not familiar with how to apply XSL in .NET:

C#:

XmlDocument xsl = new XmlDocument();
xsl.LoadXml(XSL_REMOVE_EMPTY_NODES); // constant contains the XSL above

StringWriter writer = new StringWriter();
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(xsl);
transform.Transform(new XmlNodeReader(xml.doc), null, writer);
writer.Flush();

string transformedXml = writer.ToString();


VB.NET:

Dim xsl As XmlDocument = New XmlDocument()
xsl.LoadXml(XSL_REMOVE_EMPTY_NODES) ' constant contains the XSL above

Dim writer As StringWriter = New StringWriter()
Dim transform As XslCompiledTransform = New XslCompiledTransform()
transform.Load(xsl)
transform.Transform(New XmlNodeReader(xml.doc), Nothing, writer);
writer.Flush()

Dim transformedXml As String = writer.ToString()


You can also use a Stream or XmlWriter in place of a TextWriter like I have above. I just used a StringWriter since I needed a String for the final result.

Friday, March 23, 2007

"The project you are trying to open is a Web project. You need to open it by specifying its URL path."

I hate it when I get this error even when I am opening the project from a URL. Luckily, there's a simple solution.

In the same folder as the project file (.csproj or .vbproj) of the web project you are trying to open, make a new file with the same name as the project file with .webinfo tacked on the end.

Thus, if you have a project called MyWebProject.csproj, you create a new file called MyWebProject.csproj.webinfo. In that file, it needs a short XML snippet:


<VisualStudioUNCWeb>
<Web URLPath="http://localhost/MyWebProject/MyWebProject.csproj" />
</VisualStudioUNCWeb>


Obviously, you need to replace the URLPath value with the actual value for your web project file. Once you have this hint in place, Visual Studio should be able to load your web project without a hitch.

This seems to be Visual Studio .NET 2003 problem. I've not seen it manifest itself in Visual Studio .NET 2005 yet.

Sunday, February 4, 2007

Trust in ADO.NET

In reviewing some C# code for a project at work, I'm constantly running across the following construct in the code (and this is from different developers, not just one):


DataSet ds = SqlHelper.ExecuteDataSet(sConn, CommandType.StoredProc, sProc, aParams);
// ... snip ...
DataRow row = ds.Tables[0].Rows[0];
int someInt = Int32.Parse(row["intColumn"].ToString());


Now, while this code works like you would expect, there's still something wrong with it. You're basically wasting cycles on mindless conversions.

Whenever you use ADO.NET DataRow objects, it is true that the return from the indexer is a generic Object, so something needs to happen to get the result into a strongly-typed value. But, if the database column is a SQL int, ADO.NET already returned you an Int32!

What's written above is basically the same as this:


int a = 1;
int b = Int32.Parse(a.ToString());


You're wasting cycles first converting the Int32 to a String and then using Int32.Parse to get it back to the Int32 that it was in the first place. Trust in ADO.NET!


DataSet ds = SqlHelper.ExecuteDataSet(sConn, CommandType.StoredProc, sProc, aParams);
// ... snip ...
DataRow row = ds.Tables[0].Rows[0];
int someInt = (int)row["intColumn"];


There we go, much better. If you want something that feels a little "safer" than a direct cast, feel free to use Convert.ToInt32; it at least knows not to waste time doing needless conversions if it's already passed an Int32.

As a final thought, I'll leave you this completely failed code to think about:


DataSet ds = SqlHelper.ExecuteDataSet(sConn, CommandType.StoredProc, sProc, aParams);
// ... snip ...
DataRow row = ds.Tables[0].Rows[0];
bool someBool = (row["bitColumn"].ToString() == "1");


(Hint: The reason this code didn't work like the developer expected is because the DataRow already returned a Boolean.)

Merge Modules

In my previous post, I mentioned a method for automating the addition of features (specifically properties) to a Windows Installer package generated by Visual Studio. After doing some more research, I encountered the wonderful land of merge modules.

Merge modules are just what they sound like; they allow you to merge features into a Windows Installer package that you are creating. Among other things, you can set default properties by including a Property table in the merge module.

Creating a merge module is not difficult but requires a bit of knowledge. Once again, you need the lovely Orca tool from the Windows Platform SDK.

Orca, from what I found, doesn't seem to be able to create a blank merge module template. So I had to do it by hand. Starting with a blank new file, you will need to add the following schema:

TableColumnPKTypeNullable
ComponentComponentXString (72)N
ComponentId String (38)Y
Directory String (72)N
Attributes Short IntN
Condition String (255)Y
Key Path String (72)Y
DirectoryDirectoryXString (72)N
Directory_Parent String (72)Y
Default_Dir Local String (255)N
FeatureComponentsFeature_XString (38)N
Component_ String (72)N
ModuleComponentsComponentXString (72)N
ModuleIDXString (72)N
LanguageXShort IntN
ModuleSignatureModuleIDXString (72)N
LanguageXShort IntN
Version String (32)N


Some of these tables are predefined in Orca, so you won't need to enter all of them by hand. Most of the tables don't actually have to have data, they just need to exist for the file to be considered a valid merge module.

The only table that requires data is the ModuleSignature table. It must contain at least one row identifying the merge module. The format for the ModuleID is a GUID separated by underscores (_). The Language is 1033 for US English; you will need to look this code up if you are using other locales. Version is the typical Windows Installer version format: n.nn.nnnn.

Once you have that basic structure in place, you can enter your additions into the merge module. The merge module can have pretty much anything a full install package can have. Once you're done, save the file with a .msm extension. You can then go in to Visual Studio and add the merge module to your deployment project.

Technically, there is one more thing you really should have in your merge module: a _Validation table. While not strictly necessary for use, this table is used by Orca to run validation against the merge module.

One caveat I did find with using a merge module to preset properties in deployment project builds: you can't use a merge module to override existing entries in the Property table. You can only create new properties. I was able to get the majority of the setup I wanted done automatically, I still have to go in a manually edit my final .msi file to accommodate all of my requirements.

Monday, January 29, 2007

Working with Setup projects

One thing I don't have much experience with is the Setup project type in Visual Studio. Most of my previous experience comes from writing web applications, so there was no real need to write an installer package.

But now that I've been tasked over the past 6 months or so with writing a desktop application, I've had to learn a lot of new tricks. The Windows Installer is one of those tricks. Unfortunately, the documentation for the actual Setup project type itself is rather abysmal and mostly just links into documentation for the Windows Installer SDK.

So today I was trying to figure out how to set default values for my custom properties. Unfortunately, the interface inside Visual Studio has no means for doing this. I did some reading, and I found that the tool I want is called Orca.

Orca is part of the Windows Platform SDK. It's no small download, but it contains lots of useful tools and documentation, of which the Windows Installer SDK is a part. Orca is a handy little editor that lets you modify the tables inside a .MSI package (among other things).

To set defaults for my properties, I just compile my .MSI package as normal within Visual Studio. Then I can open it with Orca, go to the "Property" table, and create the properties I need.

One thing to note as well: the case of properties makes a difference. A property that is in all uppercase (like PROPERTYNAME) is considered a "public" property; public properties can be adjusted at install-time using command line switches, like follows:



msiexec /i Example.msi PROPERTYNAME=VALUE



A property with at least one lower case letter (like PropertyName) is considered "private" and cannot be modified at install-time by the user.

There's also another type of property you might find useful, called "restricted public." These are just like public properties, except that their values can only be modified by a system administrator. To make a restricted public property, just define it as a public property. Then, create a new property called SecureCustomProperties (or edit it if it exists). Set the value of this property to the names of the public properties you want to restrict, separated by semicolons if there is more than one.

Anyways, thats' the basic run-down. In the future I'm going to look into ways to automate this process.

Wednesday, January 24, 2007

When are attributes created?

Speaking of Attributes, there's often a lot of misconceptions about how they are handled by the compiler and at runtime. Some of this is because of not reading the documentation properly, but also a lot of it is compounded by the fact that there are actually different kinds of attributes that look the same but are handled differently by the compiler and the CLR.

Take, for instance, ObsoleteAttribute: this attribute, innocently enough, behaves like any other custom attribute. It is written into the compiled metadata for your assembly. It can be reflected at runtime. But, as you know, it also triggers a specific response in the compiler; it generates a warning or error informing the programmer that the marked element is deprecated.

This can generate a false conception for the developer that attributes are instantiated a compile-time by the compiler. This is not true; ObsoleteAttribute just happens to be a special case handled specifically by the compiler.

You should also be aware of a type of attribute known as a "pseudo-attribute." These attributes are not written to the compiled metadata for your assembly, and thus cannot be reflected at runtime. Again, they are typically interpreted by the compiler in some way.

For example, there is the pseudo-attribute AssemblyVersionAttribute. This attribute tells the compiler what version to assign to the final assembly produced. If you attempt to reflect this attribute at runtime, you will find that it doesn't actually exist on your assembly (you have to use Assembly.GetName().Version).

The ultimate thing you have to keep in mind is that attributes are not instantiated until someone requests them. The compiler does this on its own in certain cases, but for the most part it just does a compile-time verification of the code and then passes the attribute on as meta-data into the IL.

For an example, assume that we have defined a custom attribute called TimeStampAttribute that takes a single string in its constructor that it wants to parse into a DateTime value. We want to use this to mark when we created certain methods:

C#:

[TimeStamp("fred")]
public void MyMethod()
{
// ...
}


VB.NET:

<TimeStamp("fred")> _
Public Sub MyMethod()
' ...
End Sub


But wait! That code won't work, right? It's supposed to have a date/time that we can parse like "1/24/2007 11:46 PM" instead of "fred." We're going to get a FormatException as soon as we run, right? Wrong.

The code of course compiles just fine, since "fred" is a string like we asked for. The code will even run just fine for the most part since the attributes aren't created explicitly. You won't get the expected exception raised until you make a call to GetCustomAttributes() that ends up iterating over the attributes in question.

Tuesday, January 23, 2007

When iterating attributes, make sure you get the right thing

I'll admit I kind of dubbed out on this one. When you're trying to get the custom attributes off of an object, make sure you're getting the right object. In my case, I needed to get the Attributes of my assembly, not the Assembly class itself.

It should come as no surprise that the following code doesn't do what I expected:



object[] attribs = Assembly.GetExecutingAssembly().GetType().GetCustomAttributes(typeof(Attribute), false);



Instead I just needed to remove the call to GetType():



object[] attribs = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(Attribute), false);



The Assembly class provides a handy GetCustomAttributes() method since calling it on the Assembly type itself doesn't really help you much. The first example gets you a bunch of crazy attributes that really aren't what I was looking for; the second returns all of the assembly-decorating attributes declared in my AssemblyInfo.cs.

Saturday, January 20, 2007

Attempting to reach remote resources with C#

This is related to the project in my previous post. Now that we had the IVR working correctly, we needed a way to move the voice recordings from the IVR server to a web server where our customers can retrieve them. Last year I wrote a web service called ResourceProxy that accomplishes the task of moving files from one server to another very nicely and with very strict security restrictions (it requires authentication, it can restrict the paths and file types allowed, etc.). Unfortunately, the IVR is not a web server, it's a phone server. So installing IIS to allow web connectivity was not a solution we wanted to use.

So what we have are two file shares. One is read-only and allows me to pick up the recordings from the phone server, and a second is read/write and allows me to drop them off on the web server. I have to authenticate using two different domain accounts to access each share, and of course the web service that accomplishes this is running as the ASP.NET machine account.

So this creates a small problem. How does one authenticate a process running as the ASP.NET machine account to allow it to connect to remote servers? Looks like we need to get down and dirty with P/Invoke.

My first solution was elegant. I would use LogonUser to authenticate each account, impersonate the principal, and then connect to the file share UNCs. The code looks something like this:


[DllImport("advapi32.dll")]
private static int LogonUser(string lpszUsername, string lpszDomain,
string lpszPassword, int dwLogonType, int dwLogonProvider,
ref IntPtr phToken);

[DllImport("advapi32.dll")]
private static int DuplicateToken(IntPtr hExistingToken, int ImpersonationLevel,
ref IntPtr phNewToken);

public void ImpersonateUser(string domain, string user, string password)
{
IntPtr token = IntPtr.Zero;

int result = LogonUser(user, domain, password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT, ref token);

if (result != 0)
{
IntPtr newToken = IntPtr.Zero;
result = DuplicateToken(token,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
ref newToken);

if (result != 0)
{
WindowsIdentity id = new WindowsIdentity(newToken);
WindowsImpersonationContext context = id.Impersonate();
}
}
}


By the way, that's the incredibly pruned-down code. If you want a more extensive example, look at the WindowsImpersonationContext class.

This code worked beautifully. I could impersonate each of the domain accounts I needed to access the remote UNCs and copy my files over. Basically I just had to impersonate the first user, load the file into a MemoryStream, impersonate the other user, and write it to disk. (This was an acceptable approach because the files in question are always fairly small, typically less than 50k.)

It worked great on the development server, which is a Windows 2003 machine. Unfortunately, the production web server is still running Windows 2000. When we tried it there, everything broke. Argh!

For some reason, on Windows 2000 I was getting a bad username/password every time I tried to impersonate one of the domain principals, even though the exact same username/password combos worked fine on Windows 2003. I tried several different permutations of parameters to the LogonUser function to see if that would help. Eventually I ended up locating a Windows 2000 box on our development domain and tried my code there, just to see if it was an environment issue in production. It didn't work there either. Same code, same users and passwords; it just wouldn't work when moving from Windows 2003 to Windows 2000.

One thing that is mentioned on the page for LogonUser at MSDN is that on Windows 2000, the account executing LogonUser must have the SE_TCB_NAME privilege. You can grant this privilege to an account (such as the ASP.NET machine account in my case) by going in to the Local Security Policy for the server and adding the account to the "Act as a part of the operating system" policy. I tried this, and again it was no go.

So after fighting with that for a few hours, I decided to try a different approach. My boss mentioned that one technique he used in a classic ASP application to achieve the effect I wanted was to map the UNCs as network drives in the code before accessing them. This had some merit, so I investigated how to achieve this in C#.

The function we want this time is called WNetAddConnection2:


[DllImport("mpr.dll")]
private static int WNetAddConnection2(
ref NETRESOURCE lpNetResource,
string lpPassword, string lpUsername, int dwFlags);

public void MapNetworkDrive(string unc, string drive, string user, string password)
{
NETRESOURCE pRes = new NETRESOURCE();
int result = WNetAddConnection2(ref pRes, password, user, 0);
}


On the surface, this code looks a lot simpler, but I actually wrote a lot of peripheral code to make sure I wasn't trying to connect to the same resource twice and also to make sure I didn't map over an existing logical drive on the server. It doesn't hurt anything to map the resource twice, because WNetAddConnection2 will tell you what happened, but WNetAddConnection2 itself can be a somewhat costly operation, so I figured it better to avoid calling it if I can.

This code worked beautifully as well. After I had mapped the network shares I wanted, I could use the typical .NET IO classes to access the UNC directly without a problem.

Of course, after running it on Windows 2000, it broke again. Argh and double argh!

For some reason, on Windows 2000 this code kept throwing back an ERROR_WRONG_TARGET_NAME. According to the documentation I could find, this meant that the UNCs were not correct (target name referring the server destination). So why would the UNCs be correct on Windows 2003 but not on Windows 2000?

Who knows? At this point I was so frustrated with it I was willing to try anything. What I found is that if I specified the IP address (\\192.168.1.1\share as opposed to \\servername\share), Windows 2000 was perfectly happy with it. And now the code works fine in production, so I'm not going to play with it anymore.

Wednesday, January 17, 2007

How not to write an IVR

So for my first post here, let me take a minute to tell you a little about my most recent project at work. We're putting together an IVR for one part of a very large suite of applications. This suite of applications was supposed to be released on January 1st, but of course now it's January 17th and we're only just now getting to true integration testing.

Unfortunately we outsourced the IVR software to supposed "experts," which is what landed us in trouble. Their technical person who has been writing the software that controls the IVR script is a complete and utter tool. He doesn't know the basics of C++ let alone software development in general.

For instance, last night I sat on the phone with him for an hour-and-a-half whilst I debugged his code for him. I should point out that I am by trade a web designer and a managed code developer that normally uses Microsoft .NET. I have not touched C++ in about ten years, so I was using Google a lot and still filling in holes in his code. Meanwhile, the only insight he really offered was, "Well I can't imagine why it would be doing that!" Well, duh, that's why you're crappy code doesn't work, Einstein.

Basically the IVR script iterates through a bunch of prompts, gathering data from the caller, and posts each response to a web service. Should be pretty straightforward, right? He was convinced that it was my web service and not his code that could be the problem. Eventually I tracked it down to one prompt in the IVR that was destabilizing the entire application and causing it to crash (eventually crash, that is, which is why it was so damn hard to find). Code similar to the following was the culprit:

char filename[200];
memcpy(filename, basepath, 200);


What's wrong with this? We're copying one string into another, but the destination is not initialized or allocated to anything. He's literally writing bytes into no-man's land. Pointers and memory allocation are a hard concept to grasp for most beginning programmers, granted, but someone who works for a company writing C++ code full-time and charging ungodly amounts of money for that time should not be making such elementary errors.

Once I fixed this error for him, the program stopped crashing. Luckily I had hung up on him by that point, so he avoided my wrath.

While I'm at it, here's one more gem of his code (excuse me if it's not perfectly compilable, I'm doing this from memory):

j = 0;
for (i = 0; i < strlen(promptfiles); i++)
{
if (promptfiles[i] == '|')
{
memcpy(prompts[prompt_index], promptfiles + j, i - j);
j = i + 1;
prompt_index++;
}
}


This little nugget was meant to parse a string of IVR prompt sound files I was giving to him from the web service in a pipe-delimited format like:


Prompt1Name|ivr_prompts\prompt1.wav|Prompt2Name|ivr_prompts\prompt2.wav|...


According to him there was a problem with one of our prompts called "Timeout" that wouldn't play correctly. Can you spot the problem in his code? I'll give you a hint; "Timeout" appeared at the end of the pipe-delimited string.

The answer is pure lack of brainpower on his part. He only copies the part of the string he wants after he hits a pipe delimiter. The string wasn't terminated by a pipe, so he was just dropping off the last part of the string and not saving it.

I guess the real mystery is why he didn't just use strtok.

Anyways, that's my rant for this evening. I promise I'll start putting some happier and more informative posts here later; for now I just needed to get this off my chest.