Auditing Part 4 - Quit being a slob

Before I start, I'd like to think my colleagues Alex Robson and Craig Israel for helping with the design. It'd have ended up containing 200% more suck without their assistance. Also, the finished product is part of Nvigorate, so whenever Alex updates that project, it'll be there. What is currently up there however is very out of date. It's improved greatly.

Welcome to Part 4, the last in my auditing series. Here's the previous ones, you drunken slob, Mr. I'm-Too-Lazy-To-Click-In-The-Archive:

Everything is almost wrapped up! Our list-o-crap has now turn into a sentence-o-crap, not even worthy of a list anymore! What is that sentence, you ask? Why, it's coming up! Pay attention! Here it comes! Right now! Firing our auditor is still noisy, as it takes place in our functions that are handling unrelated tasks. So let's clean it up!

I can't carry it for you Mr. Frodo, but I can carry you

Auditing is more of something that the function should support, but it'd be nice to not have to muddle up our business code with it, even if it is just one line.

So what's a way we could do this? Why, attributes of course! If you're not familiar with adding attributes to a function/class, here's the quick version. If you know this, tough. I'm not going to identify when I'm not talking about it, so you have to wade through these words anyways. Deal.

First, an attribute is represented (in C# that is - noone cares what it looks like in VB) by square brackets above your function/class. Like this:

   1: [MyClassAttribute]
   2: public MyClass
   3: {
   4:     [MyFuncAttribute]
   5:     public void MyFunc()
   6:     {
   7:         /* do stuff */
   8:     }
   9: }

See? Easy. Applying attributes is called "decorating" your class or function. So, we're going to go about creating some attributes that will fire up our AuditManager when our function is accessed. This is known as Aspect Oriented Programming, or AOP. But how do we do this? We're going to use an AOP framework. There are several out there, but Nvigorate already makes use of PostSharp, so we're going to use that one. Why PostSharp? Most AOP frameworks have the attributes being evaluated at run-time. This can, unfortunately, lead to some serious run-time speed hits. PostSharp, however, is run pre-compilation, and actually injects your code in the appropriate places. This means no run-time overhead, but more setup on the developers side. To do this, it has to be physically installed on the machine doing the compilation. Also, any project that makes use of the attributes (or aspect, as I'll refer to it from here on), even if not directly using PostSharp libraries, will need a reference to the PostSharp DLLs from the GAC.

So, you've installed PostSharp, and added a reference to the DLLs in your consuming project. Let the fun begin!

Insert witty subtitle here

We'll start with the basic information our aspect needs - the action, the username, the object to be audited, and the date. Aka, the same 4 things we've been looking at this entire series. One of the restrictions though about making this an aspect, is that they can't be generic! Oh noes! Well, the good news is we can still fire our AuditManager, using Reflection, and get our generic call! Yay! There's another issue too, but we took care of it when we wrote AuditManager. Remember how I had us make TWO TrackAction calls? We ended up with TrackAction for instances, and TrackActionEnumerable for collections? Well, if we hadn't done that, our reflection calls would've actually failed, as it wouldn't have been able to distinguish between a generic type and a collection of a generic type. But we're so smart, we knew that ahead of time, so we're good. What we'll do is check if our information object implements IEnumerable or not, then we'll know which one to call.

Now, I don't feel like writing raw Reflection code. Yet again, I'll be using the Reflector portion of Nvigorate to handle that for me. One of things we'll have to pass to the call is a collection of arguments. Let's think about how we'll retrieve each of those:

  • Action - well, this is just an enum type, so we can pass that right in the attribute itself. We'll add it to the constructor, then save it in a private field.
  • Date - we'll just use DateTime.Now, of course. If the auditors themselves wish to use a different timestamp, they can.
  • Information - I'm going to force a restriction here. I'm saying that consumers have to have this passed in on the function call. You're welcome to change this behavior if you desire. But I think separating your DAL from the rest of your code will end up with a cleaner design, and then this won't be an issue!
  • Username - hmmm....this one is tricky. And gee, it's like I did them out of order for a reason! Guess what, I did, suckers! That way I can make this it's own topic. Let's discuss it more.

What's in a name?

Ah, getting the username. And the system username is useless. We need to know the user who's logged in and making the call. There's a LOT of options here. Maybe it's the current user principal. That could be in a windows app, or in a web app. Maybe it's passed along the function call. Maybe it's in your request header.

The point is, we have NO idea where it could be. And trying to define every possibility is asking for failure. This becomes pretty obvious pretty quick that it's an area we need to leave open for the consuming application to define.

We should make a base class for our aspect then. Since it'll be incomplete, lacking an actual way to get the user name, it should be abstract too. "AuditAspectBase" sounds good. We'll define an abstract function then, let's call it "GetUser", that developers will have to implement to use our system. Now, we don't have to care! Yay!

The last thing our class needs to do is make sure that our concrete aspect definitions can get to the information it needs. Lucky enough, PostSharp allows us to grab all kinds of good information about the function call. From it, we grab the instance the function was called on, the parameters the function has, and the values of those. There's more too, but those are the important ones. All of this is passed through a "MethodExecutionEventArgs" object, which is defined in the PostSharp assembly. In the interest of making the consuming developer know as little about PostSharp as possible, we're going to take out the important bits, and store them in our base aspect for easier consumption.

Give me bits, dammit!

Alright, alright, alright. I've been making with lots of the werdz and none of the codez. The good news is, there's nothing left to discuss! Here's our AuditAspectBase:

   1: [Serializable]
   2: public abstract class AuditAspectBase : OnMethodBoundaryAspect
   3: {
   4:     private AuditAction _action;
   5:     private string _functionArgumentName;
   6:     private object _information;
   7:  
   8:     protected object Instance { get; set; }
   9:     protected object[] FunctionArguments { get; set; }
  10:     protected ParameterInfo[] FunctionParameters { get; set; }
  11:  
  12:     protected AuditAspectBase(AuditAction action, string functionArgumentName)
  13:     {
  14:         _action = action;
  15:         _functionArgumentName = functionArgumentName;
  16:     }
  17:  
  18:     public sealed override void OnEntry(MethodExecutionEventArgs eventArgs)
  19:     {
  20:         Instance = eventArgs.Instance;
  21:         FunctionArguments = eventArgs.GetReadOnlyArgumentArray();
  22:         FunctionParameters = eventArgs.Method.GetParameters();
  23:  
  24:         _information = GetArgumentValueByName(_functionArgumentName);
  25:  
  26:         CallAudit();
  27:     }
  28:  
  29:     public object GetArgumentValueByName(string name)
  30:     {
  31:         foreach (ParameterInfo info in FunctionParameters)
  32:         {
  33:             if (info.Name == name)
  34:             {
  35:                 return FunctionArguments[info.Position];
  36:             }
  37:         }
  38:  
  39:         return null;
  40:     }
  41:  
  42:  
  43:     private void CallAudit()
  44:     {
  45:         object[] args = new object[]
  46:                             {   _action,
  47:                                 GetUser(),
  48:                                 _information,
  49:                                 DateTime.Now
  50:                             };
  51:  
  52:         if(_information is IEnumerable)
  53:         {
  54:             Reflector.CallMethod(typeof(AuditManager), "TrackActionEnumerable", args, _information.GetType().GetElementType());
  55:         }
  56:         else
  57:         {
  58:             Reflector.CallMethod(typeof(AuditManager), "TrackAction", args, _information.GetType());
  59:         }
  60:  
  61:     }
  62:  
  63:     public abstract string GetUser();
  64: }

But I'm such a nice guy, that even though you're an ungrateful jerk, I'm going to go through this with you anyways.

First you'll notice it's marked as Serializable. This is required by PostSharp, so do it. Next you'll notice we inherit from "OnMethodBoundaryAspect". This is a PostSharp class (Laos specifically) that will help make things simpler for us. It'll handle the weaving of the aspect for us. If you don't know what that means, don't worry about it. But this specific one, will allow us to provide "hooks" into our function calls. It's going to give us functions we can override, like "OnEntry", "OnExit", etc. Feel free to check the class out. But the one we're going to focus on is "OnEntry". This does mean there is some room here to get fancy - perhaps you're only interested in auditing when the function exits, or want to make sure your audit doesn't write if an exception occurs. Things like this, and more, are possible. But, our implementation is only going to make use of OnEntry. We're getting a smidge ahead of ourselves though.

What do we need in our constructor? Well, from our previous list, we only need two things given to us - the action, and the function parameter that will have our object we're auditing. Using the information PostSharp will give us, we really only need the parameter name. So, our constructor will require those two, and we'll save them in private fields.

Next is our OnEntry method. We'll override the base one, and I'm going to seal it because I don't want it screwed with. We're going to get the calling instance, the arguments, and the parameter names from the event args, and store them in public properties. Our consuming aspects might need this to get the user name. We'll also need to get the object that's being audited. Then, we can call our auditors. We're going to break both of those into their own functions.

First is GetArgumentValueByName. Pretty obvious what it does. It'll loop through our parameter info, and once it finds a match, it'll return the argument itself. If it can't find a match, it'll return null. Because our attributes can't be generic, it can only return an "object". This function is pretty useful though, so lets make it public. It'll come in handy again soon.

The last piece is actually firing up our audit manager, in a function called "CallAudit". We want to control when this happens, so we'll make it private. The first thing we need to do, is create our argument array. Both of the AuditManager's TrackAction calls take the same 4 parameters in the same order. So, we'll grab the action, call our abstract GetUser function that our subclass will have defined, grab the object we're auditing, and our time stamp.

Next we'll check if our object is a collection, by seeing if it's of type IEnumerable. If it is, we'll use Reflector (part of Nvigorate, for those of you not following along) to call TrackActionEnumerable. If not, we'll just call the regular TrackAction. Reflector.CallMethod() has several overloads, including ones to call generic functions. First we tell it the type of our class we want to call (AuditManager), then the function name, then we pass in our arguments. If it's our collection, we need to get the type of collection, then the type of the elements it holds. Else, we can just get the type.

Last thing there is our abstract "GetUser" function. All we need from it is a string, which would be the username.

2 + 2 = 4

Time to add it all up, and see what we get! We're done now with our base aspect, so how do we implement and consume it? Easy. Let's make a couple of simple ones that are liable to have lots of reuse potential. Well, one obvious one is using this in an asp.net app. All we need to do this is:

   1: [Serializable]
   2: public class AspNetAuditAspect : AuditAspectBase
   3: {
   4:     public AspNetAuditAspect(AuditAction action, string functionArgumentName) : base(action, functionArgumentName)
   5:     {
   6:     }
   7:  
   8:     public override string GetUser()
   9:     {
  10:         return HttpContext.Current.User.Identity.Name;
  11:     }
  12: }

Doesn't get much simpler. To consume it then, all we do is:

   1: [AspNetAuditAspect(AuditAction.Update, "data")]
   2: public void SaveMyData(MyFirstType data)
   3: {
   4:     MyDataLayer.WriteData(data);
   5: }
   6:  
   7: [AspNetAuditAspect(AuditAction.Update, "data")]
   8: public void SaveMyDataCollection(List<MyFirstType> data)
   9: {
  10:     MyDataLayer.WriteDataCollection(data);
  11: }

Our aspect, and audit manager, and auditors themselves All handle the rest! Wether it's a collection or an instance. That's pretty sweet.

Here's another good aspect you might want to make use of (and yes, both of these will be included in Nvigorate when it gets updated):

   1: [Serializable]
   2:  public class FunctionArgumentAuditAspect : AuditAspectBase
   3:  {
   4:      private string UserNameArgument;
   5:  
   6:      public FunctionArgumentAuditAspect(AuditAction action, string functionArgumentName, string userNameArgument)
   7:          : base(action, functionArgumentName)
   8:      {
   9:          UserNameArgument = userNameArgument;
  10:      }
  11:  
  12:      public override string GetUser()
  13:      {
  14:          return GetArgumentValueByName(UserNameArgument).ToString();
  15:      }
  16:  }

This will handle the case where the user name is in the function call itself. Perhaps it's a webservice function or some such. So, that'd obviously need to be passed in on the constructor for our aspect. Then, using our GetArgumentValueByName function we defined in our base aspect, retrieving that value is simple. All we need to do then, to consume it this time, is this:

   1: [FunctionArgumentAuditAspect(AuditAction.Update, "data", "username")]
   2: public void SaveMyDataService(string username, MyFirstType data)
   3: {
   4:     MyDataLayer.WriteData(data);
   5: }

Now, we've managed to get the code noise out of our function, and auditing just "happens" for us. Our code is cleaner, better organized, and more flexible. This will make it easier to implement, easier to maintain, and easier to enhance.

This is the end, my only friend, the end

My favorite part is when I was done writing all this. Hopefully you can now go forth and audit like a man! Let's see that smile!

No more tears!

That'll do horse. That'll do.

0 comments:

Designed by Posicionamiento Web | Bloggerized by GosuBlogger