There are times when I’ve found that well considered and thought-through EPiServer scheduled job settings have been changed in environments where they really shouldn’t be. People usually have some reason for doing what they’re doing but it’s hard to follow if you’re unable to figure out why, and if nobody can remember doing it. I’ve had this audit log functionality on my TODO list for some time now, and finally had some time to implement it. In case you are using some sort of dependency injection, you will want to restructure and split up the code to allow for testability. Source code may be found at GitHub as usual.
Audit logging and required reasons in EPiServer Scheduled Jobs
The code is quite straight forward. EPiServer’s scheduled job interface is customized using a PageAdapter (see Surviving IE, or: How to render different markup depending on devices and browsers for a more detailed explanation) that attaches a few event handlers and adds a couple of HTML tags to the page. The handlers are triggered when the web administrator either starts a job manually, stops a job or saves the scheduled job settings.
The added reasons text box has a required field validator attached to it in order to remind the administrators to fill it out.
Of course there is nothing preventing people from giving bogus reasons making them look like all those commit messages you may find throughout some projects, but at least you will know who did it and when. Apart from the obvious, the new scheduled job settings are also saved in the log.
On the assumption that one might not be interested in what happened twenty entries back too often, there is a limit on the number of audit log entries shown on the scheduled job pane. To ease those rare occations when you do need to go digging, is is possible to download the entire audit log history as a HTML table file openable in Microsoft Excel.
Customizing EPiServer’s scheduled job interface
If you look in the EPiServer source code, you will find that the parts responsible for rendering the scheduled job interface is located in DatabaseJob.aspx; this is the file that we will need to work with. Basically we will do two things: attach event handlers to the three EPiServer ToolButton objects in order to write to the EPiServer database, and add the audit log presentation table rendering log entries.
DatabaseJobAdapter.cs
protected override void OnInit(EventArgs e) { base.OnInit(e); AddStylesheet(); AddAuditLogComponents(); AttachClickEvents(); } private void AttachClickEvents() { var saveButton = GeneralSettingsControl .FindControlRecursively("saveChanges") as ToolButton; saveButton.Click += Save_Click; var runButton = GeneralSettingsControl .FindControlRecursively("startNowButton") as ToolButton; runButton.Click += Run_Click; var stopButton = GeneralSettingsControl .FindControlRecursively("stopRunningJobButton") as ToolButton; stopButton.Click += Stop_Click; }
The GeneralSettingsControl is a Panel object that is retrieved in the same fashion as the ToolButton buttons above; with a simple FindControlRecursively extension on the Control object.
ControlExtensions.cs
public static class ControlExtensions { public static Control FindControlRecursively(this Control root, string controlId) { if (controlId.Equals(root.ID)) { return root; } return (from Control control in root.Controls select FindControlRecursively(control, controlId)) .FirstOrDefault(c => c != null); } }
When the web administrator clicks one of the buttons, an AuditLogEntry is added to EPiServer’s dynamic data store containing necessary information on the performed action. Here the administrator also have the opportunity to explain why they’re doing what they’re doing by filling in a required text field before pushing the buttons.
AuditLogEntry.cs
[EPiServerDataStore( StoreName = "AuditLogEntry", AutomaticallyCreateStore = true, AutomaticallyRemapStore = true )] public class AuditLogEntry { public string PluginId { get; set; } public DateTime Timestamp { get; set; } public string Username { get; set; } public string Email { get; set; } public JobAction Action { get; set; } public string Message { get; set; } public AuditLogEntry() { Timestamp = DateTime.Now; Username = EPiServer.Security.PrincipalInfo.CurrentPrincipal.Identity.Name; Email = EmailFor(Username); } private static string EmailFor(string username) { var profile = EPiServer.Personalization.EPiServerProfile.Get(username); return profile.EmailWithMembershipFallback ?? "N/A"; } }
On the other end, an audit log table is generated using EPiServer’s built-in styles. For clicks on the Save button, a record of the settings is also added to the message column for traceability.
DatabaseJobAdapter.cs
private static Control LogTableFor(IEnumerable<AuditLogEntry> entries) { var logTable = new HtmlGenericControl("table"); logTable.Attributes.Add("class", "epi-default"); logTable.Controls.Add(Headers()); var tbody = new HtmlGenericControl("tbody"); foreach (var entry in entries) { tbody.Controls.Add(RowFor(entry)); } logTable.Controls.Add(tbody); return logTable; }
In order to use page adapters you will need to update your adapter mappings file in the App_Browsers directory.
App_Browsers\AdapterMappings.browser
<?xml version="1.0" encoding="utf-8"?> <browsers> <browser refID="Default"> <controlAdapters> <adapter controlType="EPiServer.UI.Admin.DatabaseJob" adapterType="EPiServer.CodeSample.ScheduledJobAuditLog .DatabaseJobAdapter" /> </controlAdapters> </browser> </browsers>
Nice!
Very nice!
Very good article!
But its specific to scheduled jobs..
How can it be made generic to be used for all setting/pages audit.
Making it generic might be tricky as it is based on adapting the class that EPiServer uses to render it’s job page. You would have to create similar adapters for the pages where you’d like the functionality in order to hook up events and render output etc.