Auditing Part 3 - Because you can't get a build error with config files
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:
UniReligion!
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.
Goooooooaaaaaallllll
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.
"oh."
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:
1: public abstract void LoadConfiguration(XElement xml);
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:
1: <AuditorConfiguration>
2: <Auditors>
3: <Auditor type="MySolution.Auditors.MyFirstTypeAuditor, MySolution.Auditors"
4: handlesType="MySolution.BusinessObjects.MyFirstType, MySolution.BusinessObjects"
5: exclusive="true">
6: <Config>
7: [custom config shizzle here if needed]
8: </Config>
9: </Auditor>
10: <Auditor type="MySolution.Auditors.MyGenricAuditor`1, MySolution.Auditors"
11: handlesType="MySolution.BusinessObjects.MyBaseType, MySolution.BusinessObjects">
12: <Config>
13: [custom config shizzle here if needed]
14: </Config>
15: </Auditor>
16: </Auditors>
17: </AuditorConfiguration>
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":
1: public class MyGeneralAuditor<T> : AuditorBase<T> where T : MyBaseType
2: {
3: public override void TrackAction(AuditAction action, string user, T information, DateTime? when)
4: {
5: MyDataLayer.WriteData(new AuditLog()
6: {
7: Action = action,
8: User = user,
9: Information = SerializeObject(information),
10: Date = when ?? DateTime.Now
11: });
12: }
13:
14: public override void LoadConfiguration(XElement xml) {}
15: }
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:
1: <AuditorConfiguration>
2: <Auditors>
3: <Auditor type="MySolution.Auditors.MyFirstTypeAuditor, MySolution.Auditors"
4: handlesType="MySolution.BusinessObjects.MyFirstType, MySolution.BusinessObjects"
5: exclusive="true">
6: <Config>
7: [custom config shizzle here if needed]
8: </Config>
9: </Auditor>
10: <Auditor type="MySolution.Auditors.MyGenricAuditor`1, MySolution.Auditors"
11: handlesType="MySolution.BusinessObjects.MyBaseType, MySolution.BusinessObjects">
12: <Config>
13: [custom config shizzle here if needed]
14: </Config>
15: </Auditor>
16: </Auditors>
17: <IgnoreNamespaces>
18: <IgnoreNamespace namespace="MySolution.Notifications" />
19: </IgnoreNamespaces>
20: <IgnoreTypes>
21: <IgnoreType type="MySolution.BusinessObjects.AuditLog, MySolution.BusinessObjects" />
22: </IgnoreTypes>
23: </AuditorConfiguration>
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:
1: public class AuditConfigurationSection : ConfigurationSection
2: {
3: [ConfigurationProperty("Auditors")]
4: public AuditorCollection Auditors
5: {
6: get { return ((AuditorCollection)(base["Auditors"])); }
7: }
8:
9: [ConfigurationProperty("IgnoreTypes")]
10: public IgnoreTypesCollection IgnoreTypes
11: {
12: get { return ((IgnoreTypesCollection)(base["IgnoreTypes"])); }
13: }
14:
15: [ConfigurationProperty("IgnoreNamespaces")]
16: public IgnoreNamespacesCollection IgnoreNamespaces
17: {
18: get { return ((IgnoreNamespacesCollection)(base["IgnoreNamespaces"])); }
19: }
20: }
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:
1: [ConfigurationCollection(typeof(Auditor), AddItemName = "Auditor")]
2: public class AuditorCollection : ConfigurationElementCollection
3: {
4: protected override ConfigurationElement CreateNewElement()
5: {
6: return new Auditor();
7: }
8:
9: protected override object GetElementKey(ConfigurationElement element)
10: {
11: return ((Auditor)(element)).TypeName;
12: }
13: }
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:
1: public class Auditor : ConfigurationElement
2: {
3: private XElement _configurationXml = new XElement("blank");
4:
5: [ConfigurationProperty("type", DefaultValue = "", IsKey = true, IsRequired = true)]
6: public string TypeName
7: {
8: get
9: {
10: return (string)(base["type"]);
11: }
12: set
13: {
14: base["type"] = value;
15: }
16: }
17:
18: [ConfigurationProperty("handlesType", IsRequired = true)]
19: public string HandlesType
20: {
21: get
22: {
23: return (string)(base["handlesType"]);
24: }
25: set
26: {
27: base["handlesType"] = value;
28: }
29: }
30:
31: [ConfigurationProperty("exclusive", IsRequired = false)]
32: public bool Exclusive
33: {
34: get
35: {
36: return (bool)(base["exclusive"]);
37: }
38: set
39: {
40: base["exclusive"] = value;
41: }
42: }
43:
44: public Type AuditorType
45: {
46: get
47: {
48: return Reflector.GetType(TypeName);
49: }
50: }
51:
52: public XElement ConfigurationXml
53: {
54: get
55: {
56: return _configurationXml;
57: }
58: }
59:
60: protected override bool OnDeserializeUnrecognizedElement(string elementName, System.Xml.XmlReader reader)
61: {
62: _configurationXml = (XElement)XElement.ReadFrom(reader);
63:
64: return true;
65: }
66: }
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.
1: [ConfigurationCollection(typeof(IgnoreType), AddItemName = "IgnoreType")]
2: public class IgnoreTypesCollection : ConfigurationElementCollection
3: {
4: protected override ConfigurationElement CreateNewElement()
5: {
6: return new IgnoreType();
7: }
8:
9: protected override object GetElementKey(ConfigurationElement element)
10: {
11: return ((IgnoreType)(element)).Type;
12: }
13: }
14:
15: public class IgnoreType : ConfigurationElement
16: {
17: [ConfigurationProperty("type", DefaultValue = "", IsKey = true, IsRequired = true)]
18: public string Type
19: {
20: get
21: {
22: return (string)(base["type"]);
23: }
24: set
25: {
26: base["type"] = value;
27: }
28: }
29: }
30:
31: [ConfigurationCollection(typeof(IgnoreNamespace), AddItemName = "IgnoreNamespace")]
32: public class IgnoreNamespacesCollection : ConfigurationElementCollection
33: {
34: protected override ConfigurationElement CreateNewElement()
35: {
36: return new IgnoreNamespace();
37: }
38:
39: protected override object GetElementKey(ConfigurationElement element)
40: {
41: return ((IgnoreNamespace)(element)).Namespace;
42: }
43: }
44:
45: public class IgnoreNamespace : ConfigurationElement
46: {
47: [ConfigurationProperty("namespace", DefaultValue = "", IsKey = true, IsRequired = true)]
48: public string Namespace
49: {
50: get
51: {
52: return (string)(base["namespace"]);
53: }
54: set
55: {
56: base["namespace"] = value;
57: }
58: }
59: }
"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:
1: <section name="AuditorConfiguration" type="MySolution.AuditingConfiguration.AuditConfigurationSection, MySolution.AuditingConfiguration" />
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:
1: private static AuditConfigurationSection _configSection = null;
2:
3: protected static AuditConfigurationSection ConfigSection
4: {
5: get
6: {
7: if (_configSection == null)
8: {
9: _configSection = (AuditConfigurationSection)ConfigurationManager.GetSection("AuditorConfiguration");
10: }
11:
12: return _configSection;
13: }
14: }
"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.
1: private static bool IsIgnoredType<T>()
2: {
3: Type objectType = typeof(T);
4:
5: foreach (IgnoreNamespace ignoreNamespace in ConfigSection.IgnoreNamespaces)
6: {
7: if(objectType.Namespace == ignoreNamespace.Namespace)
8: {
9: return true;
10: }
11: }
12:
13: foreach (IgnoreType ignoreType in ConfigSection.IgnoreTypes)
14: {
15: Type ignoredObjectType = Reflector.LoadType(ignoreType.Type);
16:
17: if(ignoredObjectType.Equals(objectType))
18: {
19: return true;
20: }
21: }
22:
23: return false;
24: }
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.
1: private static List<AuditorBase<T>> GetAuditors<T>()
2: {
3: Type objectType = typeof(T);
4: List<AuditorBase<T>> auditors = new List<AuditorBase<T>>();
5:
6: foreach (Auditor a in ConfigSection.Auditors)
7: {
8: Type configuredObjectType = Reflector.LoadType(a.HandlesType);
9:
10: bool typeMatch = configuredObjectType.Equals(objectType);
11: if (typeMatch || objectType.IsSubclassOf(configuredObjectType))
12: {
13: Type auditorType = Reflector.LoadType(a.TypeName);
14:
15: if (auditorType != null)
16: {
17: AuditorBase<T> auditor = null;
18: if (auditorType.IsGenericTypeDefinition)
19: {
20: auditor = (AuditorBase<T>)Reflector.MakeGenericInstance(auditorType, objectType);
21: }
22: else
23: {
24: auditor = (AuditorBase<T>)Activator.CreateInstance(auditorType);
25: }
26:
27: auditor.LoadConfiguration(a.ConfigurationXml);
28:
29: if(a.Exclusive)
30: {
31: auditors.Clear();
32: auditors.Add(auditor);
33: return auditors;
34: }
35:
36: auditors.Add(auditor);
37: }
38: }
39: }
40:
41: return auditors;
42: }
43: }
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:
1: public static void TrackAction<T>(AuditAction action, string user, T information, DateTime? when)
2: {
3: if (ConfigSection != null && !IsIgnoredType<T>())
4: {
5: GetAuditors<T>().ForEach(a => a.TrackAction(action, user, information, when));
6: }
7: }
8:
9: public static void TrackActionEnumerable<T>(AuditAction action, string user, IEnumerable<T> information, DateTime? when)
10: {
11: if (ConfigSection != null && !IsIgnoredType<T>())
12: {
13: GetAuditors<T>().ForEach(a => a.TrackAction(action, user, information, when));
14: }
15: }
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!
1: AuditManager.TrackAction(Action.Update, currentUser, myData, DateTime.Now);
2:
3: (or)
4:
5: AuditManager.TrackActionEnumerable(Action.Update, currentUser, myData, DateTime.Now);
6:
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!
9:48 AM | Labels: Coding | 0 Comments
New template
As you can tell, I've been working on switching templates. In the time I've spent searching templates, then having to tweak them tons to get them how I want, I should've just written it myself from scratch. Anyways, this isn't done yet. I need to switch the header image to something I don't hate, and there's still some display issues with IE (surprise I know). I hope to have it ironed out soon. I like it better. Good news is, I don't care if you do or not! But it's gotta better than reading long blog posts in a stupid narrow-ass column.
7:48 PM | Labels: General | 0 Comments
Auditing Part 2 - From one to many, in the blink of an eye!
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.
Edit: Craig Israel dropped a good point to me - by using the new keyword in my derived classes, classes using the base class won't actually get the sub-classed version of my function! So, I've made the modifications needed below. If you hadn't read the blog yet, then just ignore this line.
So, hey, I actually am writing part 2! I'm as shocked as you are! If you ended up here first, feel free to check out the first part, where I introduced generics to this solution.
When we left off there, we had several things non-optimal about our solution. In fact, it was damn near unusable if you ask me. Here our current list of things to unsuckify:
- Handling collections - right now, everything is running against a single instance. This is a limitation we'll remove.
- 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.
So, let's start at the top!
Gotta collect 'em all!
Why do we care about collections? Many swanky ORM solutions allow you to have a whole buncha objects, and save them at once, instead of looping through them. So, we need to account for that. Lets refresh our head goo and see what our interface looks like again:
1: public interface IAuditor<T>
2: {
3: void TrackAction(AuditAction action, string user, T information, DateTime? when);
4: }
5:
So, what needs to change? Let's think about what actually IS changing here. Do we still the action, user, and time stamp? Yes. Do we still need the data? Yes. Is our data type still unknown to us? Hmmm...not so much. We know we're going to be getting a collection, which is important to us, but we still don't care what it' a collection of. Well, what do all collections (Lists, Array, etc.) have in common? That's right! IEnumerable. Congratulations, I'm not going to berate you in the face with mean words today. So, we want a collection of generic objects. That's easy! IEnumerable<T>.
Since all that's changed is our information, and the rest of the function is the same, I think it's time for our old pal polymorphism to help us out! He's like grandfather time, except his beard is way more gnarly. So let's add another function to our interface:
1: public interface IAuditor<T>
2: {
3: void TrackAction(AuditAction action, string user, T information, DateTime? when);
4:
5: void TrackAction(AuditAction action, string user, IEnumerable<T> information, DateTime? when);
6: }
7:
Well....that kinda sucks. Now, our interface requires two functions to be implemented, wether or not the consumer actually cares about auditing collections as a whole. You know, we should be nice, and give a default implementation for the collection based one. But, you can't give function bodies in an interface. Looks like we're going to have to upgrade to an abstract class. Then, we can make our collection based function loop through and call the instance based one over and over and over. That way, if someone WANTS to handle collections separately, they can, by simply overriding it. And since we're switching from an interface to an abstract class, we'll change our naming a bit too, to match convention. How's this look?
1: public abstract class AuditorBase<T>
2: {
3: public abstract void TrackAction(AuditAction action, string user, T information, DateTime? when);
4:
5: public virtual void TrackAction(AuditAction action, string user, IEnumerable<T> information, DateTime? when)
6: {
7: foreach (T info in information)
8: {
9: TrackAction(action, user, info, when);
10: }
11: }
12: }
Why, I think that looks good! We're gonna mark the collection-based TrackAction as virtual so we can override it in subclasses.
Come together now
So, let's see how this changes our implementation? Good news - barely! All we have to do is change where it inherits from, and add the override keyword for our single-instance version of TrackAction. Then, optionally, we can override the collection based TrackAction if we want. Since we defined a body for the collection based one, however, we'll need to use the "override" keyword on the function declaration. Let's do that too, just for fun.
1: public class MyFirstTypeAuditor : AuditorBase<MyFirstType>
2: {
3: public void TrackAction(AuditAction action, string user, MyFirstType information, DateTime? when)
4: {
5: MyDataLayer.WriteData(new AuditLog()
6: {
7: Action = action,User = user,
8: Information = string.Format("<fields><field1>{0}</field1><field2>{1}</field2></fields>",
9: information.Field1,
10: information.Field2),
11: Date = when ?? DateTime.Now
12: });
13: }
14:
15: public override void TrackAction(AuditAction action, string user, IEnumerable<MyFirstType> information, DateTime? when)
16: {
17: MyDataLayer.WriteData(new AuditLog()
18: {
19: Action = action,User = user,
20: Information = "<message>Beginning auditing a collection</message>",
21: Date = when ?? DateTime.Now
22: });
23:
24: base.TrackAction(action, user, information, when);
25: }
26: }
You can see we still let the base class do our actual looping over each instance, but if we wanted to do something special, we'd just omit the call to base.TrackAction(.....). And calling TrackAction from our business code won't change at all! Whether it's an instance or collection, we're good.
It stinks!
Shut up. I know. Today's post was pretty simple, but that's because the next step is a doozy! It's going to wash the stink right out of your mouth! What's it going to be? Well, let's take a look at our Bulleted List O' Crap:
That's right, it's going to be that first one there. We're going to go for a config-driven approach to defining how our Auditors are loaded. We're going to make something we'll call our AuditManager to read the config to find out what auditors (yes, multiple!) need to be loaded per type of object. It's gonna be a good ole time down on the farm!
9:31 AM | Labels: Coding | 0 Comments
Auditing Part 1 - How I quit being a tool and created something with generics
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.
This is gonna be a multi-part series. Hopefully it'll go better than my last multi-part series, where I only wrote the first part, then decided I hated ASP.NET too much to finish the other part. I'll go ahead and give the sequel to that one right now - you do stuff. The end.
I'm going to do something that I don't see a lot of blogs or instructional media do - start with my original flawed ideas, and show the progress to a good solution. The purpose of this series isn't so much "this is how you should do auditing, morons", because there are several different ways to successfully accomplish that (although I do feel this solution is pretty robust). I'm more interested in conveying why the solution ended up like it did. That will also mean this may seem to move a little slow for senior level people, who are already past all this. But it's easy to look at a finished product and think "Wow, that makes a lot of sense. That's clean, and easy to consume." But the challenge isn't in using and understanding the end product - but arriving at that destination. Hopefully with this, you'll be able to identify some areas where you're likely to do go down a similar path, and remember that there's a better solution. It's the whole horse and water thing. But with nerds and code. So, hopefully I can help some of you other horses not die of dehydration.
You're welcome, mister horse.
The Problem
So, we need auditing. Sweet. Almost every project out there can benefit from auditing. "What's auditing?" ...wait, what? Dammit. Okay, fine, I'll back up a second. What we're talking about here is tracking changes to the system and it's data. This way, if someone "accidentally" deletes some records, you can go easily tell who did it, when they did it, and what the records were that were trashed. Or if someone claims "gee, I don't know how that email address got changed, I didn't do it! Your system sucks!", you have a reliable and full proof way to say "No, you logged in on New Years Day and changed it. It used to be supersexy08@aol.com, and you changed it to fatandlonely09@losers.com. Rough year, eh?". It helps protect both the user and the developer.
Okay, now that you've made me waste a paragraph worth of eBreath, can I continue on? Is that alright? I'm swear, I'm going to make you into glue before this is finished.
"Alright, fine, I get it. But that's crazy! That's gonna make my database size double, at minimum!" No, it won't. Don't ever try to tell me stuff again. I can't stand it when you speak on MY blog. Jerk.
So, we want to address this problem for ANY solution. Not just your current project. Let's first think, what's in common? What actually do we want to track? Here's some obvious ones:
- Who did it
- When they did it
- What action they took
- What information/data (where applicable) they were working against
"Hey, I --" Woah woah woah, I wasn't done talking yet. Just keep your comments in your mouth for a minute. Horses aren't even supposed to be able to talk. So, for this to be reusable for any solution, we need to think about what parts we DON'T care about. We don't care about:
- How is this audit information saved
- What the working data set consists of
Any solution that doesn't offer extensibility in both of those immediately fails. Now, providing a default behavior, that can be completely and easily swapped out, is an excellent idea. But for now, we're going to leave that up to the consumer.
The Beginning of Our Solution
So, it sounds to me like we have an interface brewing here. Maybe something like this:
1: void TrackAction(string action, string user, object information, DateTime when);
Ya know, I think the set of actions a user can impose is pretty limited. I'm going to make that an enum type:
1: public enum AuditAction
2: {
3: View = 0,
4: Update = 1,
5: Add = 2,
6: Delete = 3
7: }
Also, maybe some will want the datetime of when this function was called, or maybe there's cases where that's not important until they go to save the audit information. I'm going to make that nullable. This will slightly change our function. Also, let's go ahead and wrap this guy in an interface.
1:
2: public interface IAuditor
3: {
4: void TrackAction(AuditAction action, string user, object information, DateTime? when);
5: }
So, to consume this, you only have to write one function! Super easy! Here's a possible example:
1: public class MyAuditor : IAuditor
2: {
3: public void TrackAction(AuditAction action, string user, object information, DateTime? when)
4: {
5: MyDataLayer.WriteData(new AuditLog()
6: {
7: Action = action,
8: User = user,
9: Information = SerializeObject(information),
10: Date = when ?? DateTime.Now
11: });
12: }
13: }
This is making a few assumptions, that aren't important, but I'm going to explain what these magic functions and classes that don't exist are supposed to be doing. It's assuming that we have some data layer that just needs our business object, and that we're going to serialize our entire object with a function we called "SerializeObject". All of our audits are in 1 table. But you could have it writing to a log file, or the event log, or whatever. That last bit there is the null coalescing operator, in case you aren't familiar with it.
And to consume it, all we have to do is:
1: MyAuditor.TrackAction(AuditAction.Update, currentUser, (object)myData, DateTime.Now);
But, what if we wanted to do more? Maybe, depending on the type of information coming down the pipe, we want to grab specific information? Maybe we don't want the the entire object serialized. So, now we're going to have to do crap like this inside our concrete implementation of TrackAction:
1: if(information is MyFirstType)
2: {
3: MyFirstType castedInfo = (MyFirstType)information;
4: // write information to one area
5: }
6: else if(information is MyOtherType)
7: {
8: MyOtherType castedInfo = (MyOtherType)information;
9: // write information to a different area
10: }
So, we're in a mess now. What we need is, different auditors to handle different things. But, we made it an interface, so that's easy to do. But, we STILL have to cast things. That's kind of crappy. If only there was a built-in solution....
I can see clearly now, the stupid is gone
Generics, of course! What we have here is common functionality that works against an object. The object type varies, but that object type is NOT important to our infrastructure. This is why generics exist.
Now, before we continue, I need to admit something. I've understood the concept of generics for a while now, with no problem. I've been able to consume generics without a hitch. But, for whatever reason, some synapse in my brain was not firing correctly for me to grasp when it was a proper time to harness them in my designs. It was odd when it "clicked" during discussions with my coworkers that the "information" parameter should be generic. It was seriously like seeing clearly for the first time in a long time.
So how do we harness generics in this case? Well, the type we don't care about becomes our generic parameter. Convention is to use T. Then, our class name will have the T parameter added to the class name, the same way when you use generic classes, like List<> for example.
1: public interface IAuditor<T>
2: {
3: void TrackAction(AuditAction action, string user, T information, DateTime? when);
4: }
Now, we can make our concrete consumers type safe! So we'd start with this:
1: public class MyFirstTypeAuditor : IAuditor<MyFirstType>
2: {
3: public void TrackAction(AuditAction action, string user, MyFirstType information, DateTime? when)
4: {
5: MyDataLayer.WriteData(new AuditLog()
6: {
7: Action = action,
8: User = user,
9: Information = string.Format("<fields><field1>{0}</field1><field2>{1}</field2></fields>",
10: information.Field1,
11: information.Field2),
12: Date = when ?? DateTime.Now
13: });
14: }
15: }
Then, to consume it, we just need:
1: MyFirstTypeAuditor.TrackAction(AuditAction.Update, currentUser, myData, DateTime.Now);
But it still sucks
Not too shabby. Easy to implement and consume. But there's still LOTS of room for improvement here. This is nowhere near a complete solution yet. But, let's recap what we've accomplished:
- Simple interface to implement
- Easy to consume
- Type safety
- And hey, we got to make something with generics! That's always fun
And while that stuff is good, some of it isn't good enough. We'll be changing what we wrote today before we're done. And there are other areas that we need to improve:
- Handling collections - right now, everything is running against a single instance. This is a limitation we'll remove.
- 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.
Don't cry mister horse. We're gonna make it all better.What's important to remember here, is we learned a good way to spot when generics are applicable. It's when you want to perform a common set of operations around multiple types. The way the operations work are all the same, regardless of the type. You could have always handled this by looking to a common base type (often object), but you'll loose all type safety that way, and find yourself casting constantly. So, while this post doesn't leave you with anything to use for an auditor, hopefully it at least helps you learn some of the very basics of creating something with a generic parameter.
Next post, I think we're going to go ahead and get rid of that pesky "only runs against a single instance" limitation.
4:01 PM | Labels: Coding | 3 Comments
Something amazing
I'm working on such a super awesome blog post right now, that noone else will even be able to begin to truly understand it's brillance for at least 60 years.
2:30 PM | | 1 Comments