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.
One last note - today's blog post was created with:
All religions, all the time!
Harnessing this amazing new technology allows me to create content that references all religions equally, so noone feels left out or offended! Onto the post!
If you are religious, I highly recommend you call your preacher/pastor/rabbi/spongebob/etc, because Hell may have just frozen over!!!
That's right! I actually made a third part in a series. It's crazy. Here's links to the first two parts, in case you suck and didn't know about my awesome blog before now:
That's also right, that's a bulleted list mother-fucker. And that's also also right, I dropped the f-bomb on my blog. Deal with it (unless you're my boss, and in that case, I mean, c'mon...it's not like you're honestly surprised are you? and second of all, this isn't even my blog! It's just a coincidence/result-of-a-hacker/internet-explorer-bug!)
So what's our next step? Once again, let's break out the Bag O' Crap and see:
- What if we want multiple auditors for a single type? We'll have to add more and more TrackAction calls.
- Firing our auditor - it's in the middle of our functions. That adds a bit of code noise, and can make it harder to track down.
Ah yes. That top one. This is gonna be a long post. I was in a good mood until I realized I had to go through this with you. I hate you so much. It's not my fault you're not me and don't remember doing all this already.
So, currently, you'd have to explicitly call a concrete implementation for each of your auditors. So that's dumping more code in your existing code, and if you need to fire multiple auditors, that's multiple calls. It's going to get messy, and you're going to have to remember when to call which auditors, and make sure you didn't forget any, and when you add a new auditor go and plaster it everywhere, blah blah blah.
What we have here is a single set of logic. We know the relationship of auditors to the objects we're editing. We need to store all this mess in a single place. Let's call this place our "AuditManager" ("OMG THATS THE CLASS NAME IT'S GONNA BE I CAN JUST FEEL IT"). Now, we could have this so-called "AuditManager" ("WOAH IT'S USED TWICE IT'S SO GONNA BE THE CLASS NAME") contain all the logic explicitly. Now when we add auditors, we're restricted to changing our code in 1 place. But how about we just make it so we don't have to edit any code at all?
"WOAH ROB THAT SOUNDS LIKE THE DEVILS MAGIC I DON'T TRUST THE DEVILS MAGIC THIS IS CODE ITS SERIOUS BUSINESS YOU'RE A BAD PERSON I'M GOING TO PRAY TO MY PREACHER/PASTOR/RABBI/SPONGEBOB/ETC FOR HIM TO SAVE YOU FROM THIS DEVILS MAGIC"
Seriously, quit it with the caps, jack ass. If you weren't so busy being a jack ass as to not read the title, you'd know where this is going. Go ahead, read it. I'll wait.
Yeah. That's right, speak in a small font. You should be embarrassed. We're gonna put all that stuff in config files, and let our AuditManager (yes it's the class name shut up for god/jesus/buddha/spongebob/etc's sake) determine which auditors to fire up.
It's like code, but instead of semi-colons you use less-than and greater-than symbols
If you seriously believe this heading, you should stop right now. Maybe go to a "your money buys you pre-built opinions to mimic!" platform.
Now noone likes over-abundant XML, so we're gonna try to make this as simple as possible, and only focus on must-have features. Due to the complexity of the upcoming pieces, we're gonna end with the full feature-set. This is *not* the order it was written in - it expanded and changed throughout development. Unlike in Part 1, I think it'd be more confusing to show the natural development of this piece. We'd constantly keep jumping around. And in part 1 it was a design decision, and this is more of a feature set. So, I think it's less crucial, and if you disagree, go write your own damn blog talking about how stupid I am.
And just in case you're unfamiliar with config files, we're talking about embedding this in our respective app.config or web.config files (depending on what type of solution you are building).
First, we'll need a root containing tag. We'll call it "AuditorConfiguration" (wow, it's like the name tells you what it means! How novel!). Inside it, what do we need? Well, our auditor definitions. Well call this block "Auditors". And then we'll list each "Auditor" inside its separately. Now what do we need to know about each auditor? Well, we will need to know the type of the auditor itself (so we can fire it up), as well as for what type it should handle.
Let's discuss that for a second. Done. LAWL, GET IT I LITERALLY WAITED A SECOND.
Sorry. I pulled a you.
Anywhos, It's going to do more than just "handle a single type". What if you want ALL your business objects to be audited, regardless of anything evarz, but maybe here and there you want add something extra? And maybe you have a case or two where you don't want ANY other auditors to handle it? So, this is more than just a single type. If you put a base class in here, that all your business objects inherit from, they'll all get audited. We'll make sure our AuditManager checks for inheritance. For the other case, we'll need to be able to explicitly state in our config file that it's an exclusive auditor...why let's make the attribute "exclusive"! This "naming crap what it means" is neat!
Now, let's say you have all your auditors in a single assembly, but your project is tiered. Maybe sometimes you need to audit things on the front end of the system, but other times on the back end. Depending on where in your technology stack you are you will save your audits differently. You don't want to have to write multiple auditors that do the same thing. So, we're going to leave this completely undefined, but we're going to support the ability to embed a "config" section for your auditor. You'll have different config files depending on where your auditor lives, and each one can specify how that data gets saved. How you interpret that in your auditor and want to use that is up to you. I recommend going with something that doesn't suck.
However, we'll need to make a quick change to support this. We should add something to our base class, or we won't have a way to know how to pass the custom config in! All we need to add to our AuditorBase<T> is:
Making it abstract will force our consumers to provide a definition. This is done to hopefully force users to think about how they want to save their audits, and come up with something flexible. You could make it virtual if you don't want to enforce that. If you're not familiar with defining your own config schema and options, you will be by the time we're done today, so you can do so easily. Or you can just parse the XML that will get passed. I'd like to get more in-depth, but any solution is really going to be more domain-specific, and what this system does (or at least it's goal) is provide a flexible way of auditing regardless of your domain.
So, it sounds like our configuration section is coming along nicely. Let's take a peek at what it looks like so far:
Time for another round of "explain the made up crap" - This is saying you have a solution. Everything in this solution will start with the "MySolution" namespace. There are two projects in this solution that this is concerned with - "MySolution.BusinessObjects" and "MySolution.Auditors". Those are your assembly names as well. All your business objects inherit from "MyBaseType". So, for naming our types, we give the full namespace name for our class, then a comma, then the assembly name where they live. This is called a fully qualified domain name, usually abbreviated as FQDN. But you already knew that. But the window cleaner behind you in your Skyscraper of Power has been reading this article with you, and he was confused. I'm always helping out the little people.
"MyFirstTypeAuditor" hasn't changed since we last saw it in Part 2, except for defining LoadConfiguration. But what's this "MyGenericAuditor`1" you ask? More specifically, what the hell is that "`1"? That's how you define generic types in a config file. Here's our class definition for "MyGenericAuditor":
Notice that this auditor is generic itself! Crazy sauce! We've specified, though, that it only works on types that come from our base type, cleverly called "MyBaseType". Also notice that our magical "SerializeObject()" function has made a return. So, since the definition for this class is generic, and you can't put "<" or ">" in an attribute definition for XML, we do that with ~1. Note that if our class definition had multiple generic types (say "MyGenericSomething<T, P, S>"), it would be defined in a config file like "MyGenericSomething~1~2~3". But, our AuditManager that we'll write soon won't be supporting that, so it's irrelevant to us at the moment. Just trying to be nice to those who don't know. Geez, lay off.
There's another feature I think we should support in our config too though. Maybe we have some business objects we DON'T want to audit? A very possible realistic use of this is putting it at the entry points to your data layer. But your AuditLog class will go through there too! Hello, infinite loop, our old friend. Or maybe you have notifications as well that you log to a database, but don't care about auditing. So, we need to tell our AuditManager to ignore some stuff. Let's give it two ways to do that - by types, or by namespace. That'd expand our config block to look like:
Alrighty then. That looks pretty good. I think we're done here.
"But what about--" nope. Not happening. We're done. I'm tired of your ruining my life.
Go colon-right-parenthesis yourself somewhere else.
Microsoft WANTS you to put crap in a config file
Swear to god/jesus/buddha/spongebob/etc they do. Why? Because they give you some classes that keep you from having to parse all the XML yourself. It'll turn your XML into objects, which is spiffy. Easy to use too. At each node of the configuration block, we define a new class that defines what attributes and children it can have. I'm going to go through this pretty quickly. It's not difficult, and MSDN can explain stuff for you in more detail if you need it. Everything that you need is contained in the "System.Configuration" namespace. Add a reference to the dll in your project if you don't already have one.
We'll start at the top level, and keep defining our classes as we go down. If you're playing along at home, doing it in this order is kinda bass-ackwards, since we'll have not-yet-defined class names. But it's easier to explain this way, so deal.
The first block is the AuditorConfiguration. It's defined as such:
So, your root section needs to inherit from "ConfigurationSection". Then, each of your properties in this class that are represented in the config file, need to have the attirbute "ConfigurationProperty", which takes 1 parameter - what it's named in the config itself. Each of our sections from our top level contain multiples, so that's why they're each named "Collection". We don't want to set them in our client code, just read what's there, so we're only going to define a getter (and no setters). To retrieve them, we call "base" with an string index, that is the same as the parameter you passed to the "ConfigurationProperty". Then you'll want to cast that to your property type (in our case, our various collection classes).
Let's move to our "AuditorCollection" class then. It looks like this:
Since this is a collection, you inherit from "ConfigurationElementCollection". You decorate this class with a "ConfigurationCollection" attribute, which takes one parameter - the type of of the actual configuration element it contains (which we'll name Auditor to match our XML). We're also going to specify what tag name it should look for to add new items, which is "Auditor" again. We do that with "AddItemName" property.
"ConfigurationElementCollection" also requires us to override two functions. The first is "CreateNewElement", and all we do there is return our "Auditor" configuration element class that we haven't defined yet. The other is "GetElementKey". We know our elements are all of type "Auditor" in this collection, so we'll cast it, and grab a property that we'll treat as the key and return it. In our case, it'll be the name of the type of auditor it represents.
Lastly, lets define our "Auditor" element. Here are teh coedz:
Our single element inherits from "ConfigurationElement". We have a private field for that user-defined config block. We tag our properties that are attributes in our XML properly, marking key and required as needed. Each one exposes a get and a set, which just go against the base collection. We also made a Type property that uses Reflector. Reflector is a part of Nvigorate, and is one of it's most useful features. Reflector simplifies all your reflection calls, as you can see.
The last thing we do is override "OnDeserializeUnrecognizedElement". This will be raised whenever that user-defined config block is hit. So, we take that, and store it in our field.
We repeat this for IgnoreTypes and IgnoreNamespaces. I'll show them here for you, but they both follow the same setup as AuditorCollection and Auditor, except simpler, so I'm not going to explain them.
"Waaaah, you're going too fast! I need more detail!" Stuff it, you big baby. This blog isn't for babies. It's for real men who punch live animals to death every night, or they don't get to eat.
Last thing we'll have to do, is let the configuration manager know to link up our configuration classes with the config elements. In the configuration/configSections of your config file, add this:
Once again, I'm just making up a namespace, but what you have to do is just put the FQDN in for the "type" attribute of where your "root" class lives.
Less code = less chance for you to screw it all up
Damn this is a long post. I should've broken it up into multiples, but you'd be left with an incomplete solution. Not that I think you should use anything until the series is finished, but you could if you wanted to. Phew.
So, last piece! Praise be to jesus/buddha/god/ganesh/spongebob/etc! .We have all our auditors defined and configured and all that crap. We've mentioned that we're gonna use an "AuditManager" class to read the config and do the magic. The goal is so that we only have to call TrackAction ONCE in our consuming code, instead of calling all the auditors individually. There's no reason to change our signature on that function either. We'll need two TrackAction's, just like in our auditors - one for instances, and one for collections.
However - in the next post, you'll see we're actually going to be calling the AuditManager using reflection. I'm jumping the gun a tad, because I'm saving us the headache of having to go back and rework this piece. How this is going to affect our design is minimal, but you'll bitch at me if I don't explain it. Since we'll be calling AuditManager.TrackAction with reflection, and TrackAction will be a generic call with two overloads where the only changed parameter IS the generic one, it's going to fail for us. It won't be able to identify between "T information" and "IEnumerable<T> information". So what we're going to do is instead have TrackAction and TrackActionEnumerable.
Before our AuditManager can do anything though, it's going to need that config data! That's easy. Remember, MS wants you to use configs. And we don't want to have to load that config data over and over and over, so we're going to make it static. We'll have a private static field that will save our information for us. In our public property, when we try to access that private field, if it's null, we'll load it from the config. Else, we'll just return the private field. All you need is this:
"ConfigurationManager" is also contained in the "System.Configuration" namespace I mentioned earlier. We're going to grab our configuration, then cast it to our classes we defined earlier.
Now to start going through it. The good news for you, is that this is also easy. The good news for me, is that means you're less likely to whine at me. You access the config section just like you'd expect! The "ConfigurationManager" at this point has organized your object hierarchy so that you can just loop through your properties. You'll see that in just a second, but first let's think of the overall approach we're going to need to do here. Our two track action functions will first need to make sure our "ConfigSection" property isn't null. If it is, then that means the consuming application didn't define anything in the config (or possibly named the section incorrectly). If it's not null, we'll need to make sure then that the type isn't in the ignore list. If it passes both of those however, then we'll want to loop through all the auditor definitions and find which ones match, create an instance of them, and call TrackAction on each one.
So let's start with our easier part of this functionality - checking to see if it's an ignored type. "IsIgnoredType" sounds like a good function name for me. Our TrackAction call is generic, so let's make our IsIgnoredType generic as well. I'll show you the function, then we'll discuss it.
First thing to note is that it's static. There's no reason for us to have to instantiate our AuditManager. All of our functions will be static.
The first thing we do in our function is create a Type object of our generic parameter. We'll then start at our ignored namespaces. For each of our ignored namespaces defined in our config, we'll see if it matches the Type's namespace property. If it does, we exit the function with a return value of true.
If our type passes that check, then we'll move on to our ignored types. We'll loop through them. For each defined one, we'll once again use Reflector (that handy dandy part of Nvigorate I mentioned earlier) to load our type, then call the "Equals" function on the Type object to see if it matches our current type. The reason we load the type and call the equals function, instead of doing something simpler like say, just checking the type names, is because we want to make sure they are indeed the EXACT same type. We don't want to run into issues where classes get named the same and end up ignoring the wrong ones. Once again - if we find a match, we exit returning true.
Then if our type passes these checks, we return false.
The last thing we need to do is retrieve all our matching auditors. We're going to call that "GetAuditors". It will also be a generic function. And when it's done, it should return a list of our auditors. Now, we won't know the concrete auditor types, but that's why they have a base class in common. So, we'll return a "List<AuditorBase<T>>", that way our TrackAction calls can still fire TrackAction on each one.
Once again, I'm going to show you the code then explain it.
First things first, we create a Type object of our generic parameter. Next, we create an empty list of our return value. This way, at the end of our function, we can just return this parameter, and our consuming code doesn't have to worry about doing null checks.
Next we'll loop through all the defined auditors. For each one, we'll use Reflector to load the type, then check if the types match. If they do, OR if our type we're auditing is a subclass of this auditor's handled type, then we need to create an instance and add it to our list. If not, we continue on to the next defined auditor.
So, let's say we've found a match. Then we'll use Reflector (yet again) to load the auditor's type itself. We'll do a null check first - if you find this failing, make sure you've added a reference to the project where you've defined your auditors. Then we'll need to see if it's a generic auditor (remember, defined in the config file with the "`1" syntax). If it is, then we'll use Reflector to make an instance of that for us, passing the auditor's type as well as the generic parameter type (which will be the type created by our generic parameter that the function is running under). If not, then we'll just use the regular "Activator.CreateInstance" call.
Whichever way we load our auditor, we'll of course want to cast it to our AuditorBase<T>. Next, we'll want to pass our auditor the configuration XML that we found. Lastly, we'll want to check if this auditor is exclusive or not. If it is, then we should clear our list, only add this one, and exit. If not, then just add it to the list, and continue to the next one.
Last thing to do, is to create our actual TrackAction calls! we defined earlier what they need to do, so let's take a look at the final product, using our spiffy new functions:
You'll notice we're using LINQ there to iterate through, and call our individual TrackAction calls. You could do this with a regular "foreach" call as well, but this obviously looks cleaner. That's it!
Loose weight and impress the opposite sex!
So, let's check our consuming code now!
No matter how many auditors we add now, we only have to make one simple call. We no longer have to touch this code.
However, the fact that there is still code there is uggo-fied. That's our last major hurdle to clean up. But how could we remove actually calling our code? That's kind of important. Maybe if we could decorate the functions we needed to audit...hmm....
TO BE CONCLUDED!