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.
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 email@example.com, and you changed it to firstname.lastname@example.org. 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:
Ya know, I think the set of actions a user can impose is pretty limited. I'm going to make that an enum type:
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.
So, to consume this, you only have to write one function! Super easy! Here's a possible example:
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:
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:
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.
Now, we can make our concrete consumers type safe! So we'd start with this:
Then, to consume it, we just need:
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.