On TextWriterTraceListener, Inheritance, InitializeData, ASP.NET, and Paths
So you know that .NET has this nifty tracing framework built in; you just plug a few lines into your system.diagnostics section of your app.config or web.config file and now your Trace statements are being output to the listener that you specify. Neat.
Let’s say that you’ve gone the extra mile and you’ve implemented your own custom trace listener. Even better, let’s say that you’ve created a trace listener that extends from TextWriterTraceListener. After all, you’re probably logging to a text file, but perhaps you wanted to change the format around a little bit.
Recall that the way for us to specify the location of our trace logging file via configuration is to use the initializeData attribute as in the following example:
<system.diagnostics> <sharedListeners> <add name="Listener:ApplicationText" type="Skiviez.Hedgehog.Model.AlignedTextWriterTraceListener, Skiviez.Hedgehog.Model" initializeData="Media\Logs\Hedgehog.txt" traceOutputOptions="ThreadId, DateTime, ProcessId" /> </sharedListeners> </system.diagnostics>
Right. Looks pretty sane: I’ve got my custom trace listener type (we’ll assume that AlignedTextWriterTraceListener simply extends the built-in TextWriterTraceListener), I’m telling it to log to Media\Logs\Hedgehog.txt (relative to my working directory, I would presume), and I’m passing in some output options and giving it a name.
Nothing we have done would here would leave us to believe that we have broken the way TextWriterTraceListener works. We have, but it won’t be apparent until we try to run this in a ASP.NET Web site.
What have you got against Web sites?
So when we run it an ASP.NET Web site, we will note that while we receive no error, we also see no log file sitting in the Media\Logs directory as we specified. I can understand how the tracing code probably swallows exceptions–so as to not take down your entire Web site or application because some hoo hah misconfigured a tracing statement in the configuration file–so let’s assume that we’ve screwed something up. Check the permissions on the Media\Logs directory? Check. Check the working directory? Hmm.
The working directory of ASP.NET applications is usually strange, somewhere in the %SYSTEMDIR% area–that’s because your code is usually running from some temporary location where your ASP.NET Web site was compiled just-in-time to serve the first request. So our trace listener is trying to be relative to a highly privileged directory–not relative to our Web application’s root directory–and obviously the log file can’t be created there, failing silently.
Okay, that makes sense. But that seems astonishing somehow.
You’re not going crazy
The reason why this seems astonishing is that if you replace your configuration to use the standard TextWriterTraceListener instead of your custom type that extends from it, then the log file will magically appear in the expected location, relative to your Web application’s root directory, and not relative to the system directory.
<system.diagnostics> <sharedListeners> <add name="Listener:ApplicationText" type="System.Diagnostics.TextWriterTraceListener" initializeData="Media\Logs\Hedgehog.txt" traceOutputOptions="ThreadId, DateTime, ProcessId" /> </sharedListeners> </system.diagnostics>
Okay. So you double-check your custom type to make sure you’re really not doing anything strange with configuration. Which of course you aren’t–you’re just forwarding constructors to the base TextWriterTraceListener’s constructors. You’re not supposed to care how they actually work.
So why is the path that the initializeData attribute seems to be relative to changing wildly between the two types?
Enter the Reflector
So after spelunking through Reflector for a little while, we stumble across this little gem in the TraceUtil’s class GetRuntimeObject() method:
internal static object GetRuntimeObject( string className, Type baseType, string initializeData) { // ... snip ... if (string.IsNullOrEmpty(initializeData)) { if (IsOwnedTextWriterTL(c)) { throw new ConfigurationErrorsException( SR.GetString( "TextWriterTL_DefaultConstructor_NotSupported")); } ConstructorInfo constructor = c.GetConstructor(new Type[0]); if (constructor == null) { throw new ConfigurationErrorsException( SR.GetString( "Could_not_get_constructor", new object[] { className })); } obj2 = constructor.Invoke(new object[0]); } else { ConstructorInfo info2 = c.GetConstructor(new Type[] { typeof(string) }); if (info2 != null) { if ((IsOwnedTextWriterTL(c) && (initializeData[0] != Path.DirectorySeparatorChar)) && ((initializeData[0] != Path.AltDirectorySeparatorChar) && !Path.IsPathRooted(initializeData))) { string configFilePath = DiagnosticsConfiguration.ConfigFilePath; if (!string.IsNullOrEmpty(configFilePath)) { string directoryName = Path.GetDirectoryName(configFilePath); if (directoryName != null) { initializeData = Path.Combine( directoryName, initializeData); } } } obj2 = info2.Invoke(new object[] { initializeData }); } // ... snip ... } // ... snip ... } internal static bool IsOwnedTextWriterTL(Type type) { if ((typeof(XmlWriterTraceListener) != type) && (typeof(DelimitedListTraceListener) != type)) { return (typeof(TextWriterTraceListener) == type); } return true; }
Now, I know that I for one have written some strange and hackish code in my time, but whoever wrote this function was obviously wearing his silly pants that day. Here’s what the function does:
If the following are ALL true:
- The trace output listener type is one of the three concrete types explicitly provided by Microsoft (and not an inheritor of that type)
- The initializeData attribute is provided
- The initializeData path seems to be relative
Then the function mangles the initializeData attribute by combining it with the path of the configuration file that contained the attribute. It then instances the trace listener by providing this faked-just-in-time full path as the fileName argument.
So if I had configured a TextWriterTraceListener in my configuration file, and my configuration file were in C:\Foo, and I specified Media\Logs\Hedgehog.txt as the initializeData parameter, then the TraceUtils class would give the TextWriterTraceListener instance the value of C:\Foo\Media\Logs\Hedgehog.txt as the fileName argument.
But if I had configured anything but TextWriterTraceListener, XmlWriterTraceListener, or DelimitedListTraceListener as my output listener in my configuration file, and my configuration file were in C:\Foo, and I specified Media\Logs\Hedgehog.txt as the initializeData parameter, then the TraceUtils would kindly tell me to screw myself, pass the TextWriterTraceListener instance the value of Media\Logs\Hedgehog.txt as the fileName argument, which the instance would then match with the current working directory, which is something insane like C:\Windows\System32\Temporary ASP.NET Files if we’re talking about an ASP.NET Web site, and so I would end up with C:\Windows\System32\Temporary ASP.NET Files\Media\Logs\Hedgehog.txt, which is about as useful as sticking my thumb up my butt.
So just by extending a class, we have completely broken the way one of its configuration attributes works. That’s pretty astonishing when it comes to object-oriented design.
An ugly work-around
So I wasn’t about to specify a full path in initializeData because I don’t enjoy munging a configuration value on every single machine that I happen to build and deploy my solution to. As this is written in the .NET Framework’s internal TraceUtils class, I simply cannot avoid this behavior when extending a Microsoft-provided type.
So if I want to be able to specify a relative path in initializeData with a type that extends from TextWriterTraceListener and have it mimic the TextWriterTraceListener’s initializeData relative-path-resolution behavior, my only recourse is to copy TextWriterTraceListener out of Reflector and make it my own type with my own relative-path munging semantics.
I change the constructors of my new type, which I’ve christened TextWriterTraceListener in my own namespace, as follows:
/// <summary> /// Initializes a new instance of the TextWriterTraceListener class, /// using the file as the recipient of the debugging and tracing output. /// </summary> /// <param name="fileName">The name of the file the /// TextWriterTraceListener writes to. </param> public TextWriterTraceListener(string fileName) { this.fileName = MungeFileName(fileName); } /// <summary> /// Initializes a new instance of the TextWriterTraceListener class /// with the specified name, using the file as the recipient of the /// debugging and tracing output. /// </summary> /// <param name="fileName">the fileName to output to</param> /// <param name="name">the name of the trace listener</param> public TextWriterTraceListener(string fileName, string name) : base(name) { this.fileName = MungeFileName(fileName); }
and the gloriously-named MungeFileName() method is
/// <summary> /// Munges the file name such that if it is a relative path, we go /// relative from the configuration file and not from the current working /// directory. This makes things work as expected on ASP.NET sites and /// makes other applications similarly work with non-astonishing /// behavior. /// </summary> /// <param name="fileName">the file name to munge</param> /// <returns>the munged filename</returns> private static string MungeFileName(string fileName) { string configPath; string mungedFileName; mungedFileName = fileName; if (fileName[0] != Path.DirectorySeparatorChar && fileName[0] != Path.AltDirectorySeparatorChar && !Path.IsPathRooted(fileName)) { ConfigurationSection configSection; configSection = (ConfigurationSection)ConfigurationManager .GetSection("system.diagnostics"); if (configSection != null) { configPath = configSection.ElementInformation.Source; if (!string.IsNullOrEmpty(configPath)) { string directoryName; directoryName = Path.GetDirectoryName(configPath); if (directoryName != null) { mungedFileName = Path.Combine( directoryName, fileName); } } } } return mungedFileName; }
What I’m doing here is saying, “Is the path relative? Then figure out where my system.diagnostics configuration came from, and make myself relative to THAT directory.” This way, I don’t have to have different code paths for this class depending on whether it’s running in a traditional Windows application or an ASP.NET Web site–the initializeData is always relative to the configuration file and not the current working directory, which is what I want 99% of the time, and it’s probably what most people want, too.
Conclusions and Delusions
I think the primary lesson to take away from this is to avoid doing magic configuration and baby-sitting for a class’s constructors. I have a feeling that TextWriterTraceListener was implemented by somebody at Microsoft and sometime much later in the development process they realized that specifying a relative initializeData was difficult for ASP.NET applications. Being unable to alter TextWriterTraceListener without breaking compatibility for non-ASP.NET applications that may have already been built, they devised an unusual TraceUtils class to munge the parameters read from configuration before they were passed to a new instance’s constructor.
The result? It worked well if you stuck to the built-in classes, but sure was damn surprising if you created a custom type and eventually used it on an ASP.NET project.
Hope this helps someone out there. You’re not insane, after all.






Well that explains a lot!! :o) Thanks for the article – explains why my log files weren’t being created, though I guess they were _somewhere_.
Like the writing style!
Hi,
I have some doubt about the Parameter i.e. Filename. How can I get this Filename and what actually this filename.
My requirement is also similar to yours. But the path should be
ie.Server.Path of the application +Hedgehog.txt
It is very urgent requirement. Can you please help me on this.
Thanks
Srini