February 2007 - Posts
I've stumbled upon this article which explains how to implement delegates in Java. The issue of implementing things we have taken for granted reminded me of a course I took a few years ago, in a language called Scheme, which is a variant of Lisp. Scheme is a functional language, and has a very recursive nature. It has no loops. As exercises at this course, we had to implement things such as While and For loops and add them to the Scheme interpreter.
This made me think of how I would implement loops if didn't have them at C#. I figured I would want to have something like this:
[Test]
public void TestWhile()
{
int i = 0;
Loops.Do(delegate() { i++; }).While(delegate() { return i < 5; });
Assert.AreEqual(5, i);
}
And this is how I coded it up. LoopOptions does most of the work. Its While method accepts a condition (which is the same as the built-in Predicate<T> only it's not generic. Same goes for Action). It uses tail recursion to repeat the action. And that's it.
public delegate bool Condition();
public delegate void Action();
public class LoopOptions
{
private Action _action;
public LoopOptions(Action action)
{
_action = action;
}
public void While(Condition cond)
{
if (!cond())
return;
_action();
While(cond);
}
}
public class Loops
{
public static LoopOptions Do(Action act)
{
return new LoopOptions(act);
}
}
Now it will be super-easy to implement a repeat-until pattern, we'll just add this method to LoopOptions:
public void Until(Condition cond)
{
_action();
While(
delegate()
{
return !cond();
}
);
}
And call it like this:
Loops.Do(delegate() { i++; }).Until(delegate() { return i == 5; });
Of course this has absolutely no practical usage, but I somehow find this disturbingly fun.
Now for the reader's challenge: implement the for loop...
ScottGu has posted
a great article about Url Rewriting, which basically allows you to replace addresses such as www.mysite.com/games.aspx?gameid=HalfLife with www.mysite.com/games/HalfLife. He pretty much covers everything you would want to know on this very useful technique.
And a bonus - a very interesting architecture discussion has developed
at ayende's. Dig in.
As I've said in a previous post, I've been playing with unit-testing lately, trying to write up some classes and tests for them as I go along. I found this to be a really enjoyable experience. The tests help me understand what's going on in my code better, and they actually make me write better, and more correctly-structured code. Coupled code with too many dependencies is hard to test - by having to write tests as I go along I somehow force myself to think of a better design.
This also made me feel like up until now I was, with lack of a better expression, "programming on thin air". I had been writing lots of code, which depended on different classes, which depended on even more code, and I had no idea, no 'proof' that any of this even works. It is similar to trying to perform some mathematical calculation by relying on theorems that were never proved. There is no reason for the calculation to be right, and for all purposes it is utterly wrong. Doing this in a calculus exam will get you 0 points, but in developing software without unit-tests this is exactly what we do.
Now, I'm not saying that unit-tests are mathematical proofs. We can only test some of the values in some situations, when proofs are for all values in all situations. Still, if your tests are good and thorough enough, they can come pretty damn close, at least enough for you to feel that the ground you are standing on is stable.
About the TDD "Write tests before you write the code" approach, well, I'm still not quite sold on that. The problem is that in order to write the tests, even just to start, I've got to have a pretty good idea, in my head, of the design of my classes and interfaces. But many times, especially in the more complex scenarios, I have no idea how the code is going to look like when I start writing it. I start coding, then I refactor again and again - methods become classes, classes become interfaces, names change constantly. If I had to maintain working tests all this time, there would be hell to pay. Once the structure of the code stabilizes (which usually happens after a few hours), and I have a pretty solid idea of the final design, I can start writing the unit tests and the rest of the code. But maybe that's just me.
I've always found the new CodeBehind model of ASP.NET 2.0 to be a bit confusing. What I like to do with confusing things that I want to remember and understand better, is to write them down. Here we go.
Of the top of your head, which classes will be generated by ASP.NET for the following WebForm?
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Tester.aspx.cs" Inherits="Tester" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Button ID="Button1" runat="server" Text="Button" /></div>
</form>
</body>
</html>
And the sophisticated code file...
public partial class Tester : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
}
Looking at the temporary ASP.NET files directory, you will see 3 files:
1. App_Web_[GeneratedAssemblyName].1.cs will contain your code file definitions, meaning this:
//[Some using statements here]
public partial class Tester : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
}
#line default
#line hidden
2. App_Web_[GeneratedAssemblyName].0.cs will contain two classes. The first one is the second part of your partial class:
public partial class Tester : System.Web.SessionState.IRequiresSessionState {
protected global::System.Web.UI.WebControls.Button Button1;
protected global::System.Web.UI.HtmlControls.HtmlForm form1;
protected System.Web.Profile.DefaultProfile Profile {
get {
return ((System.Web.Profile.DefaultProfile)(this.Context.Profile));
}
}
protected System.Web.HttpApplication ApplicationInstance {
get {
return ((System.Web.HttpApplication)(this.Context.ApplicationInstance));
}
}
}
So this class contains all the control definitions, and some helper properties which give you easy access to the Profile and Application objects. This is why you can access your page controls programmatically without defining them in your code file (as in ASP.NET 1.1): They're in the second part of your class, which is generated behind the scenes by ASP.NET.
But that's not where all the actions is at, this file contains one more class (I've left only a small part of the code for this class, to focus on what's interesting for us):
1 [System.Runtime.CompilerServices.CompilerGlobalScopeAttribute()]
2 public class tester_aspx : global::Tester, System.Web.IHttpHandler {
3
4 private void @__BuildControlTree(tester_aspx @__ctrl) {
5
6 this.InitializeCulture();
7 //Lots of page parsing code deleted here
8 global::System.Web.UI.HtmlControls.HtmlForm @__ctrl2;
9 @__ctrl2 = this.@__BuildControlform1();
10 @__parser.AddParsedSubObject(@__ctrl2);
11 @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n</body>\r\n</html>\r\n"));
12 }
13
14 protected override void FrameworkInitialize() {
15 base.FrameworkInitialize();
16 this.@__BuildControlTree(this);
17 this.AddWrappedFileDependencies(global::ASP.tester_aspx.@__fileDependencies);
18 this.Request.ValidateInput();
19 }
20
21 public override void ProcessRequest(System.Web.HttpContext context) {
22 base.ProcessRequest(context);
23 }
24 }
This is the actual class that is instantiated by ASP.NET on every request. As you can see, this class inherits our Tester page, and is generated from the aspx file we created above. It all starts with the ProcessRequest method, whose base implementation will call the FramworkInitialize along the way. There the control tree of the page will get built. Check out line 11: you can see how all the 'free roaming strings' we have in our aspx file are wrapped with LiteralControls.
To summarize, for every WebForm we create ASP.NET generates for us one brother and one son. The brother is a helper that contains useful control definitions and other properties. The son is generated from the aspx file and is responsible for the page parsing and control-tree building.
But wait a second, I said 3 files, didn't I? I almost forgot this one:
3. App_Web_[GeneratedAssemblyName].2.cs:
namespace @__ASP {
internal class FastObjectFactory_app_web_kfeaunlh {
private FastObjectFactory_app_web_kfeaunlh() {
}
static object Create_ASP_tester_aspx() {
return new ASP.tester_aspx();
}
}
}
Well, actually this class is not generated for every Page we have. We'll get one of those per assembly, which usually means per directory in ASP.NET 2.0 (since by default every directory in our web site will be compiled into a single assembly). If we add another form and a user control to our directory it will look like this:
internal class FastObjectFactory_app_web_bkne5flo {
private FastObjectFactory_app_web_bkne5flo() {
}
static object Create_ASP_webusercontrol_ascx() {
return new ASP.webusercontrol_ascx();
}
static object Create_ASP_tester2_aspx() {
return new ASP.tester2_aspx();
}
static object Create_ASP_tester_aspx() {
return new ASP.tester_aspx();
}
}
As it name suggests, the purpose of this class is to quickly instantiate our forms and user-controls. Apparently, this is a kind of trick that allows the instantiation of the types by reflection to be faster. You can read more about it in Fritz Onion's post.
Writing all this down really helped to set things straight in my head - hopefully you'll find it useful as well. In a sequel for this post, I intend to write about another confusing (and very related) subject - the ASP.NET 2.0 compilation model. Stay tuned.
I'm a unit-testing newbie, but I'm trying to get into it (as I realized, one day, how scared I was to change working code someone else wrote a long time ago). Trying to test some of my code using NUnit failed completely, since that code tried to cache stuff, and the calls to HttpContext.Current.Cache threw a null reference exception. Well, silly me, of course that wouldn't work. There's no HttpContext when the unit test is running (it's not running in a web application), and therefore no Cache object. Or is there...?
Apparently you can use the ASP.NET cache in non web applications, as I found out thanks to this post. All you have to do is call HttpRuntime.Cache instead of HttpContext.Current.Cache and you're good to go. Of course, that will mean adding a reference to the System.Web assembly in my WinForms/Windows Service applications, which might be a bit awkward, but I guess I can look the other way in order to gain a great caching mechanism in non web applications, and a testable caching method in web applications.
At work we had to create a windows service for a certain purpose. We were glad to find out that Visual Studio allows you to easily create one of those and install it using the installutil command.
When we tried to debug our service, which was running on our local Windows 2000 systems, we ran into some problems. The first one is well known - you can't debug the service unless it is started already*, so how do you debug the entrance to the service - the OnStart method? There are many solutions for this, we solved it with code similar to this:
protected override void OnStart(string[] args)
{
new Thread(
delegate()
{
#if DEBUG
Thread.Sleep(20000);
#endif
StartService();
}
).Start();
}
That is, we start the service in a new thread so that the OnStart method will return right away. In debug, we stall it for 20 seconds so that we can attach the debugger.
So far, so good. But we hit another road block soon enough - when attaching the debugger to the service process, our breakpoints had the familiar "the breakpoint will currently not be hit" symbol, which means the debug symbols had not been loaded, for some reason.
After a lot of head scratching we realized the cause: the user that the service was running under was a weak user that we created. When we added the user to the admin group, the debugging worked perfectly. That is of course, a problem - we do not want our user to have admin privileges, not even in development (we can definitely expect surprises when the user becomes weaker in production). So far, we haven't found a solution for this.
Interestingly, this seems to be a Windows 2000 issue. I tried doing the same thing at home with Windows XP Pro, and the problem did not return (that is, I managed to debug a weak user service). I could not find any reference to this problem online, which led me to think we might be missing something, although I can't figure out what.
I'll keep you posted when we find the solution.
________________________________
* Actually, you can debug it if you can attach the debugger to the process quickly enough, but since the OS requires that the OnStart method return in under 30 seconds, that doesn't leave you with much time for debugging...
One thing I learned over the years is that programming can tell you a lot about yourself, and even improve you as a person. Can you handle intense pressure? Can you make the impossible possible by finding creative solutions?
Imagine the bug of your nightmares creeping into your code days before the deadline. This bug, like all really annoying bugs, is happening randomly, caused by no apparent reason, and is impossible to solve. Think segmentation-faults in your university C++ project 10 hours before it's due (and of course, at first it looks like the error happens only you're looking away from the screen or while eating a banana.)
Now there's a character-building moment. Will you crash under the pressure and give up? Or will you step up, be a man (or a woman, I'm no sexist) and face the thunder? Here is some advice for those who choose the latter, inspired by The Pragmatic Programmer, a must-read book for any programmer.
- Take a deep breath. Go drink something (I mean coffee or water. I find that alcohol is bad for bug-solving).
- The bug is not random - you or one of your teammates got their names on it - and it is not impossible to solve. Tell this to yourself.
- Remind yourself the last time you thought a bug was random/impossible. You felt rather stupid at the end, didn't you?
- Tell a teammate, or anybody else (yes, even your mother) about the bug. Sometimes you'll realize the solution as you speak (and the authors of The Pragmatic Programmer suggest that if you don't have anyone to talk to right now, you can try a rubber duck).
- Google is your friend.
- Get some sleep. Sometimes things seem so much clearer at the morning. With a fresh mind you'll get better results.
- Don't be shy; Ask for help. Your teammates, online forums, or all at once. You can post about it in your blog if you have one.
- Think about this as a riddle you're trying to solve. Write down the options. Rule out things. Focus on others.
Believe me, these tips work. And trust me when I say, once this ultra-difficult bug is solved, you will feel either really stupid (because you forgot to assign that variable) or really proud (because you managed to solve that deadlock in a creative way). Either way - you'll end up a better person for it. What doesn't kill you...
I actually wrote this post for myself, as I am now facing one of these massive-looking bugs. Every time that happens, I need to remind myself of all of this, so I decided to write it down at last, and also share it with you. I will now follow advice number 6, go to sleep, and probably dream some bug-related dreams.
And for you, Mr.Almighty Bug: I will beat you, you slimy ***. It's just a matter of time before your head will be in the ground, together with the rest them.
Recently, I've encountered the following code, in a commercial API for a software that costs dozens of thousands of dollars. Now you don't need to know what this method is supposed to do, all you need to know is that Geometry is an abstract base class for all types of, well, geometries, and Point inherits from Geometry.
public DataTable[] Identify(Geometry geom)
{
//... Do some stuff ...
if (!(geom is Point))
throw new NotSupportedException();
//... Do some more stuff ...
}
If you're wondering how I got to see the code - I used Lutz' Reflector for that.
Now, silly me, I thought that when a method accepts a Geometry parameter, it should work for all types of geometry, and not only for points, so of course I was extremely surprised to see this unclear exception get thrown in my face.
You might say, "maybe they intended to add the functionality in the future". Well, that's no excuse. They could supply an overload just with a Point parameter for now.
Anyway, the amusing thing is that this method does work for other types of geometries. All you have to do is to copy the code from the Reflector, remove the weird exception throwing, and it works like a charm. I don't understand how this kind of bug could get into production code. Makes you wonder if they even test their code.
The software in question is ESRI ArcGIS Server 9.2, which is one of the world's leading products in supplying server-based GIS. Go figure.
Today my teammate Matan has sent me a piece of code I wrote a while ago, which he had to read for some reason.
The code had the following comment:
//Changed by Doron Yaacoby: 9/7/2006, WC Finals. Forza Italia!!!
I love to leave amusing little comments in my code, especially on special occasions/circumstances. It's always fun to look back later at the code and reminisce in that little piece of history. Nostalgia is always a fresh breath of air, especially if you're trying to fix something in an old piece of code.
Also, if you're debugging someone else's 3 years old code, while cursing him every single second, you might cut him some slack if you see a comment such as: //Added on 31/12/2003, 23:32 :(
So my suggestion is to leave these little historical notes around. They certainly can't hurt, and they will bright up someone's sad debugging night in a year from now.
And my question to you - what kind of funny comment did you run into lately?
Check this out. This 14 year old kid called Shachar has lectured about Windows Vista in front of a crowd of grown-up professionals at the Microsoft Developer Academy a few days ago (which I attended and really enjoyed). Man, when I was 14 I barely knew what an operating system does. And they say today's youth spend all their time watching TV and IM-chatting...
At the last part we talked a little about post compiling, AOP and how we can use the PostSharp tool to make our code look a lot better. At this part I want to get a little more deep inside the mechanism behind this cool feature, and I'll do this by first showing another example.
The Logging Attribute
Let's say you want to create an attribute that enables you to log a method, including when you entered the method, the arguments it received and it's processing time. Sounds useful, no? Let's see how easy it is to do it with PostSharp.
First we'll create our attribute.
[global::System.AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public sealed class LoggingAttribute : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionEventArgs eventArgs)
{ }
public override void OnExit(MethodExecutionEventArgs eventArgs)
{ }
}
For now it does nothing, but you can see we already have some hooks we can use to log the entrance and the exit of the method. Also notice that we marked the attribute as Serilizable. It won't compile otherwise (we'll see why later on). In order to create the headers for our log messages, we'll override a different method: CompileTimeInitialize.
[Serializable]
[global::System.AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public sealed class LoggingAttribute : OnMethodBoundaryAspect
{
private string _entranceMessage;
private string _exitMessage;
public override void CompileTimeInitialize(System.Reflection.MethodBase method)
{
base.CompileTimeInitialize(method);
string methodDescription = method.DeclaringType.FullName + ": " + method.Name;
_entranceMessage = "Entering " + methodDescription;
_exitMessage = "Exiting " + methodDescription;
}
public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
}
public override void OnExit(MethodExecutionEventArgs eventArgs)
{
}
}
As you can see, this method gets a MethodBase as a parameter, which supplies us with the name of the method and it's enclosing type. We use this to create both an entrance message and an exit message. As the name suggests, CompileTimeInitialize is called during compile time, so the string concatenations that go on here we'll be called only when we compile method. It hardly matters for this small example, but may hold a sigfinicant performance value for more intense calculations. Anyways, let's move on and see the completed attribute:
[Serializable]
[global::System.AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public sealed class LoggingAttribute : OnMethodBoundaryAspect
{
private string _entranceMessage;
private string _exitMessage;
public override void CompileTimeInitialize(System.Reflection.MethodBase method)
{
base.CompileTimeInitialize(method);
string methodDescription = method.DeclaringType.FullName + ": " + method.Name;
_entranceMessage = "Entering " + methodDescription;
_exitMessage = "Exiting " + methodDescription;
}
public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
base.OnEntry(eventArgs);
Console.WriteLine(_entranceMessage);
object[] arguments = eventArgs.GetArguments();
if (arguments != null && arguments.Length > 0)
{
Console.WriteLine("Method arguments: ");
for (int i = 0; i < arguments.Length; i++)
{
if (i != 0) Console.Write(",");
Console.Write(arguments[i]);
}
Console.WriteLine();
}
eventArgs.MethodExecutionTag = DateTime.Now;
}
public override void OnExit(MethodExecutionEventArgs eventArgs)
{
base.OnExit(eventArgs);
Console.WriteLine(_exitMessage);
Console.WriteLine("Return value: " + (eventArgs.ReturnValue ?? "null"));
DateTime startTime = (DateTime) eventArgs.MethodExecutionTag;
TimeSpan processTime = DateTime.Now.Subtract(startTime);
Console.WriteLine("The processing of the method took " + processTime.TotalMilliseconds +
" milliseconds ");
}
}
At entry we are writing to the console (although we could use any other way of logging, of course) our entrance message, and the method arguments, if it has any. Notice that we also save the time we entered the method in the MethodExecutionTag property of the eventArgs, which allows us to save state for the method execution. In OnExit we're using this property to determine how long it took the method to run. We're also printing to screen the return value of the method (making sure that if the return value is null, we'll say so). Nice and easy.
So let's place this on a method:
public class NamesBO
{
[Logging]
public string WhatIsMyName(string firstName, string lastName)
{
Thread.Sleep(1000);
return firstName + lastName;
}
}
And now let's test this...
Great, although as you can see, I forgot to place a space between my first name and my last name. Oh well.
Digging In
Now that we've got our example working, let's see what goes on underneath. Unfortunately, Ludz' Reflector seems to have an issue with methods modified by PostSharp, and is unable to disassemble them. We'll have to brush up on our IL then. Let's look on parts of the WhatIsMyName method, using the IL DASM tool.
1 .method public hidebysig instance string
2 WhatIsMyName(string firstName,
3 string lastName) cil managed
4 {
5 ....
6 System.Reflection.MethodBase::GetMethodFromHandle(valuetype [mscorlib]
7 ...
8 IL_0038: newobj instance void [PostSharp.Laos]PostSharp.Laos.MethodExecutionEventArgs::.ctor(class [mscorlib]System.Reflection.MethodBase,
9 object,
10 object[])
11 ...
12 IL_0041: ldsfld class Common.LoggingAttribute '~PostSharp~Laos~Implementation'::'aspect~2'
13 ...
14 IL_004a: callvirt instance void [PostSharp.Laos]PostSharp.Laos.IOnMethodBoundaryAspect::OnEntry(class [PostSharp.Laos]PostSharp.Laos.MethodExecutionEventArgs)
15
16
Here we can see the parts of the injected code:
- At line 6: A call to get the method data using reflection.
- At line 8: Creation of the MethodExectutionEventArgsObject.
- At line 12: Retrieval of the attribute instance (apparently the attribute object is instanstiated once, and saved in a static field).
- At line 14: Call to the OnEntry method.
Let's continue:
1 .try
2 {
3 .try
4 {...
5 IL_0074: call void [mscorlib]System.Threading.Thread::Sleep(int32)
6 ...
7 IL_007c: call string [mscorlib]System.String::Concat(string,
8 string)
9 ...
10 } // end .try
11 catch [mscorlib]System.Exception
12 {
13 ...
14 IL_0092: ldsfld class Common.LoggingAttribute '~PostSharp~Laos~Implementation'::'aspect~2'
15 ...
16 IL_009b: callvirt instance void [PostSharp.Laos]PostSharp.Laos.IExceptionHandlerAspect::OnException(class [PostSharp.Laos]PostSharp.Laos.MethodExecutionEventArgs)
17 PostSharp.Laos.MethodExecutionEventArgs::get_ReturnValue()
18 ...
19 } // end handler
20 } // end .try
21 finally
22 {
23 ...
24 IL_00ec: callvirt instance void [PostSharp.Laos]PostSharp.Laos.IOnMethodBoundaryAspect::OnExit(class [PostSharp.Laos]PostSharp.Laos.MethodExecutionEventArgs)
25 ...
26 PostSharp.Laos.MethodExecutionEventArgs::get_ReturnValue()
27 IL_00fa: castclass [mscorlib]System.String
28 IL_00ff: stloc V_1
29 IL_0103: endfinally
30 } // end handler
31 ...
32 } // end of method NamesBO::WhatIsMyName
33
Wow, that's a big piece of code there. Well, apparently PostSharps wraps our code with some try-catch-finally blocks. It does this, obviously, in order to be able to handle cases in which the method throws an exception (we still want to log the method in this case, don't we?)
- At lines 5-7: Our string concatenation takes place (first and last name).
- In 11-20: Handling of an exception, and a call to the attribute's OnException method (we didn't use it in our example, but you can guess what it's for).
- in 21-30: We are exiting the method, we call the OnExit method, and make sure we return the ReturnValue that we got from the MethodExectutionEventArgs (which means that, yes, the attribute can change the method's return value if it wants too).
The last thing I want to show you from the IL is a piece of the assembly manifest. There it is:
.mresource private '~PostSharp~Laos~CustomAttributes~'
{
// Offset: 0x00000000 Length: 0x000001E7
}
Interesting, PostSharp appears to embed a resource in our dll. The Reflector is thankfully able to show us what's in that resource:
We can see that among other things, the string "Entering Common.NamesBO: WhatIsMyName" is embedded in the dll. That's one of the strings we saved in our CompileTimeInitialize method. That's the reason we had to mark our attribute as Serializable - so all our fields we'll be serialized in the dll's resource.
Multicasting
Lets say that our NamesBO contains tons of methods, and just now we started to use PostSharp and created our LoggingAttribute. We want to have logging for all the methods, but we really don't want to go over the gazillion methods and add the attribute to them. What shall we do, then? Well, just this:
[assembly: Logging(AttributeTargetTypes="Common.BO.*")]
This magic line will cast our attribute on all the methods in the Common.BO namespace, and we're all set.
But what if we wanted to add logging to methods that are in a dll that we don't have the source for, which we can't post compile. For example, what if we wanted to trace all our calls to the System.Collections.Generic namespace? Well, there is a solution for that too, although it will require us to rewrite our attribute, to inherit from OnInvocationAspect, as we did with the CachedAttribute in Part A. We'll call this attribute LoggingExternal. You'll notice that it's pretty similar to the Logging attribute, with the main change being that we override the OnInvocation method, which actually replaces the call to the original method.
[
[Serializable]
[global::System.AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public sealed class LoggingExternalAttribute : OnMethodInvocationAspect
{
private string _entranceMessage;
private string _exitMessage;
public override void CompileTimeInitialize(System.Reflection.MethodBase method)
{
base.CompileTimeInitialize(method);
string methodDescription = method.DeclaringType.FullName + ": " + method.Name;
_entranceMessage = "Entering " + methodDescription;
_exitMessage = "Exiting " + methodDescription;
}
public void LogEntry(MethodInvocationEventArgs eventArgs)
{
Console.WriteLine(_entranceMessage);
object[] arguments = eventArgs.GetArguments();
if (arguments != null && arguments.Length > 0)
{
Console.WriteLine("Method arguments: ");
for (int i = 0; i < arguments.Length; i++)
{
if (i != 0) Console.Write(",");
Console.Write(arguments[i]);
}
Console.WriteLine();
}
}
public void LogExit(MethodInvocationEventArgs eventArgs, DateTime startTime)
{
Console.WriteLine(_exitMessage);
Console.WriteLine("Return value: " + (eventArgs.ReturnValue ?? "null"));
TimeSpan processTime = DateTime.Now.Subtract(startTime);
Console.WriteLine("The processing of the method took " + processTime.TotalMilliseconds +
" milliseconds ");
}
public override void OnInvocation(MethodInvocationEventArgs eventArgs)
{
LogEntry(eventArgs);
DateTime startTime = DateTime.Now;
eventArgs.Delegate.DynamicInvoke(eventArgs.GetArguments());
LogExit(eventArgs, startTime);
}
}
Any call to a method that this attribute is placed upon, will be replaced with a call to a new method that is wrapped with our logging code. That means, that unlike attributes that inherit from OnMethodBoundaryAspect, if you want an attribute that inherits from OnInvocationAspect to work, you have to post-compile the assembly that calls the method, and not the assembly in which the method resides. A common pitfall (well, at least I fell in it) would be to post-compile only the assembly with our BO methods, and then not understand why your OnInvocationAspect-inheriting (such as caching) attributes don't work.
After we create the attribute, we'll apply it attribute as follows:
[assembly: LoggingExternal(AttributeTargetAssemblies = "mscorlib",
AttributeTargetTypes = "System.Collections.Generic.*")]
The only problem with this code is, well, that it doesn't work. It worked for the System.Threading namespace, but it doesn't work for this namespace, and it doesn't even compile for some namespaces I use.
As you can tell, the PostSharp framework is not stable enough at the moment - In one case compiling my code caused my Visual Studio to crash. For now PostSharp is still in its beta stage, so the bugs can be understood.
Conclusion
AOP with post compiling has many advantages: clearer code, allows you to avoid duplication, and easily integrated into existing code. Also, the compile-time weaving of the code ensures pretty good performance. Of course, there are some disadvantages as well: Unexpected behavior might arise in some situations (attributes on attributes? Recursion?) and the attributes should be written very carefully, especially if they're multicasted on classes or assemblies. Also, you have to post-compile your code which makes the compilation stage a bit longer.
Still, I feel the advantages are greater than the disadvantages. Although I believe it's not stable enough at the moment to be integrated into production code, the PostSharp framework is a great one for this purpose, and I suggest that you give it a shot.
I will conclude with a question for you: remember the CachedAttribute from Part A? Well, I can only pass it a constant Cache key, since that's the only kind of parameter I can pass to an attribute. Usually, that's not good enough for cache keys (they tend to depend on the method's arguments). Any idea how can I improve on this behavior? I thought of several solutions, but none of them was solid enough. Your advice will be greatly appreciated :)