Friday, 1 July 2011

ASP.NET MVC - Audit Database Changes

You may wish to create an audit trail of database changes, i.e. creates, updates or deletes of records. This might be for internal corporate reasons (the 'blame game'), or for external regulatory reasons, e.g. FDA Part 11 compliance.

Using SQL Server or similar, you can setup a combination of triggers and stored procedures to automatically audit each change, but the simple approach presented here is to maintain the audit trail in application server code using a modified DbRepository pattern (as per NerdDinner). Once setup as described below, no other code changes or database changes are required (other than provision of appropriate views / reports etc. of the audit data).

Create an Audits Table


For example, using SQL Server:

Column Name Data Type Allow Nulls Comments
AuditID int no Primary key. Auto-incrementing.
AuditNewState nvarchar(MAX) no
AuditOperation nvarchar(10) no
AuditPrimaryKey int no This is a foreign key for one of your business tables.
AuditTable nvarchar(30) no The name of the business table for this item.
AuditTime datetime no The time of the change.
AuditUser int no The user instigating this change.


Intercept Each Database Save


If you are using the DbRepository pattern described for NerdDinner, you can simply create a base Repository class for your app, and add a Save method to the base class:

    public virtual void Save(int user)
    {
        try
        {
            if (user == 0)
            {
                // Don't audit these changes.
                DataContext.SubmitChanges();
            }
            else
            {
                // Audit these changes.
                var deletes = DataContext.GetChangeSet().Deletes;
                var inserts = DataContext.GetChangeSet().Inserts;
                var updates = DataContext.GetChangeSet().Updates;

                DataContext.SubmitChanges();

                if (deletes.Count() + inserts.Count() + updates.Count() > 0)
                    AuditChanges(user, deletes, inserts, updates);
            }
        }
        catch (Exception e)
        {
            throw e;
        }
    }

The Save method is passed some User ID which is saved with the audit item. Passing a user ID of zero will suppress the auditing, for example if you don't want to audit when importing another database.

The AuditChanges method can be something like:

        protected virtual bool AuditChanges(int user, IList<object> deletes, IList<object> inserts, IList<object> updates)
        {
            // Audit multiple change.
            var auditRepos = new AuditDbRepository(this);

            // Audit the deletes.
            foreach (var item in deletes)
                AuditChange(item, user, "delete", auditRepos);

            // Audit the inserts.
            foreach (var item in inserts)
                AuditChange(item, user, "insert", auditRepos);

            // Audit the updates.
            foreach (var item in updates)
                AuditChange(item, user, "update", auditRepos);

            auditRepos.Save(user);

            // OK.
            return true;
        }

and the AuditChange method could be:

        protected virtual bool AuditChange(object item, int user, string operation, IAuditRepository auditRepos)
        {
            // Audit a single change.
            string typeName = item.GetType().Name.ToUpper();

            string newState = "";
            int primaryKey = 0;
            string table = "";
            DateTime time = DateTime.Now;

            switch (typeName)
            {
                // Add all business classes to be audited.
                case "CUSTOMERS":
                    table = "Customers";
                    primaryKey = ((Customer)item).CustomerID;
                    newState = ((Customer)item).AuditState();
                    break;

                case "PRODUCTS":
                    table = "Products";
                    primaryKey = ((Product)item).ProductID;
                    newState = ((Product)item).AuditState();
                    break;
            }

            if (primaryKey == 0)
            {
                // Ignore this change.
                return true;
            }
            
            return AuditHelpers.AddItem(time, table, primaryKey, user, operation, newState, auditRepos);
        }


Add an Audit Item


Each audited database change causes one new item to be added to the Audits table. A new Audit object is created using the helper method below and added to the Audits repository.

namespace AuditHelpers
{
    public static class AuditHelpers
    {
        public static bool AddItem(DateTime time, string table, int primaryKey, int user, string operation,
            string newState, IAuditRepository repository)
        {
            var audit = new Audit();

            audit.AuditNewState = newState;
            audit.AuditOperation = operation;
            audit.AuditPrimaryKey = primaryKey;
            audit.AuditTable = table;
            audit.AuditTime = time;
            audit.AuditUser = user;

            repository.Add(audit);

            // OK.
            return true;
        }
    }
}


Serialize a Record


Each business class to be audited should have an AuditState extension method, probably implemented in a helper class, e.g.

        public static string AuditState(this Customer cust)
        {
            // Serialize the current field values of 'cust' for storage in an audit item.
            string result = "Name: " + cust.CustomerName + "\r\n";

            if (!String.IsNullOrWhiteSpace(cust.CustomerAddress))
                result += "Address: " + cust.CustomerAddress + "\r\n";

            result += "Tel: " + cust.CustomerTel + "\r\n";
            result += "Fax: " + cust.CustomerFax + "\r\n";

            return result;
        }

ASP.NET MVC - Report Generation from .ASPX Files

You can easily use the ASP.NET MVC view engines to create static HTML files or custom HTML reports from, for example, .ASPX and .ASCX files - no need to use some other template system!

Here's a simple example:

    public string Generate(ViewDataDictionary viewData, string templateFile, string masterFile,
            ControllerContext controllerContext)
    {
        var writer = new StringWriter();
        var tempData = new TempDataDictionary();

        var viewResult = ViewEngines.Engines.FindView(controllerContext, templateFile, masterFile);

        if ((viewResult == null) || (viewResult.View == null))
        {
            Log.WriteLine("Generate: ERROR - View not found, template file: '{0}', master file: '{1}",
                templateFile, masterFile);
            return "View not found";
        }

        var viewContext = new ViewContext(controllerContext, viewResult.View, viewData, tempData, writer);
        viewResult.View.Render(viewContext, writer);

        return writer.ToString();
    }

The viewData argument is used to pass in your model, e.g.

    var model = new MyReportViewModel(xxx, yyy);
    var viewData = new ViewDataDictionary(model);

I usually keep the .ASPX and .ASCX fies in a separate subtree of the Views folder.

Thursday, 30 June 2011

Arduino - Triggered Function Generator

This is a triggered function generator / pulse generator for the Arduino Mega using Arduino 022.

The sketch is too long to present as a snippet, but it's downloadable from here.

Features


  • Configurable via commands from the Arduino IDE Serial Monitor.
  • Triggered by external digital input.
  • Output sequence includes an optional pulse of configurable duration (in ms) starting at lag 0.
  • Main output sequence has configurable start time and duration (in ms).
  • Includes a configurable internal test source which can be looped to the trigger input.
  • Configuration can be saved to EEPROM.
  • Automatically loads configuration from EEPROM at startup.
  • At the moment this design outputs a delayed pulse, but it can easily be extended to output any arbitrary digital waveform (function), i.e. to be a digital AWG (Arbitrary Waveform Generator).
  • With the addition of provision for analog output, either using PWM functionality or an external DAC, it could be easily extended to make an analog AWG.

Commands


The commands currently implemented are:

    BEEP ON/OFF       Switch beep on/off (stimulus input)
    HELP              Display help information (this command)
    INTMODE (string)  Set the interrupt mode
                      (options are LOW / CHANGE / RISING / FALLING)
    LAG (int)         Set the lag time (ms)
    MARKER (int)      Set the marker length (ms)
    OFF               Disable the output
    ON                Enable the output
    OUTPUT (int)      Set the output length (ms)
    PULSE (int)       Set the pulse length (ms)
    RESET             Reset to default values
    RESTORE           Restore configuration from EEPROM
    SAMPLE (int)      Set the sample frequency (KHz)
    SAVE              Save configuration to EEPROM
    STATUS            Display status information
    TEST (int)        Set the test stimulus interval on pin 12 (ms)

(int) signifies an integer parameter, separated by a space.
(string) signifies a string parameter, separated by a space.

Performance


Some screenshots of example output between 50 and 1000 ms lag:


50 ms lag


100 ms lag


200 ms lag


500 ms lag


1000 ms lag

The upper trace is the stimulus input, the positive edge of which triggers both the function generator and the oscilloscope.

Description


If anyone's interested, I can add some annotated snippets here describing the code.

Acknowledgements


Interrupt usage and tips in speaker_pcm by Michael Smith (michael@hurts.ca).

C# - String Helpers - Various

Usage


Various short and fairly self-explanatory string helpers.

Snippets


using System;
using System.Drawing;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;


namespace My.Helpers
{
    public class StringHelpers
    {
        public static int Count(string text, string pattern)
        {
            // Count how many occurrences of 'pattern' there are in 'text'.
            int result = 0;

            try
            {
                int start = 0;

                while (true)
                {
                    int index = text.IndexOf(pattern, start);
                    if (index < 0) break;

                    result++;
                    start = index + pattern.Length;
                }
            }
            catch (System.Exception Ex)
            {
                Log.WriteException("StringHelpers", "Count", Ex);
            }

            return result;
        }

        public static string Get(string text, string defaultValue)
        {
            if (!String.IsNullOrWhiteSpace(text))
                return text;
            else
                return defaultValue;
        }

        public static string GetFirstLine(string text)
        {
            string result = "";

            if (!String.IsNullOrWhiteSpace(text))
            {
                int index = text.IndexOf(Environment.NewLine);

                if (index < 0)
                    result = text;
                else
                    result = text.Substring(0, index);
            }

            return result;
        }

        public static string GetMD5Hash(string text)
        {
            var x = new MD5CryptoServiceProvider();
            byte[] bs = Encoding.UTF8.GetBytes(text);

            // Compute the MD5 hash value.
            bs = x.ComputeHash(bs);

            // Convert it to a hex string.
            var s = new System.Text.StringBuilder();

            foreach (byte b in bs)
                s.Append(b.ToString("x2").ToLower());

            return s.ToString();
        }

        public static string RegexExtract(string text, string pattern)
        {
            // Return the first part of 'text' that matches 'pattern' (if any), otherwise an empty string.
            try
            {
                Regex rx = new Regex(pattern);
                Match match = rx.Match(text);

                if (match.Success)
                    return match.Value;
            }
            catch (System.Exception Ex)
            {
                Log.WriteException("ServiceTester", "RegexMatch", Ex);
            }

            return "";
        }

        public static bool RegexMatch(string text, string pattern)
        {
            // Does 'text' match 'pattern'?
            try
            {
                Regex rx = new Regex(pattern);      // , RegexOptions.Compiled | RegexOptions.IgnoreCase);
                return rx.IsMatch(text);
            }
            catch (Exception Ex)
            {
                Log.WriteException("ServiceTester", "RegexMatch", Ex);
            }

            return false;
        }

        public static string Repeat(char ch, int count)
        {
            return new String(ch, count);
        }

        public static string Repeat(string text, int count)
        {
            var builder = new StringBuilder();

            for (int i = 0; i < count; i++)
                builder.Append(text);

            return builder.ToString();
        }

        public static string Tabify(string text, int tabWidth)
        {
            // Replaces each sequence of 'tabWidth' spaces with a tab.
            // Ignores smaller numbers of spaces (similar to VS2010).
            if (String.IsNullOrWhiteSpace(text))
                return "";

            string spaces = new String(' ', tabWidth);

            return text.Replace(spaces, "\t");
        }

        public static string Untabify(string text, int tabWidth)
        {
            if (String.IsNullOrWhiteSpace(text))
                return "";

            string spaces = new String(' ', tabWidth);

            return text.Replace("\t", spaces);
        }
    }
}

C# - String Extensions - Various

Usage


Various fairly self-explanatory short string extensions.

Snippet


using System;
using System.Collections.Generic;
using System.Linq;

namespace My.Helpers
{
    // String extension methods.

    public static class StringExtensions
    {
        public static int CompareTrimmed(this string text, string str, bool ignoreCase = true)
        {
            return String.Compare(text.Trim(), str.Trim(), ignoreCase);
        }

        public static bool ContainsIgnoreCase(this string text, string value)
        {
            return (text.IndexOf(value, System.StringComparison.OrdinalIgnoreCase) > 0);
        }

        public static bool ContainsIgnoreCase(this string text, string value, out int index)
        {
            index = text.IndexOf(value, System.StringComparison.OrdinalIgnoreCase);
            return (index >= 0);
        }

        public static bool EndsWithIgnoreCase(this string text, string value)
        {
            return text.EndsWith(value, System.StringComparison.CurrentCultureIgnoreCase);
        }

        public static string Left(this string text, int maxLength)
        {
            if (maxLength <= 0)
                return "";

            int count = Math.Min(maxLength, text.Length);

            return text.Substring(0, count);
        }

        public static string ReplaceIgnoreCase(this string text, string value1, string value2)
        {
            string work = text;
            int start = 0;
            var caseInsensitive = System.StringComparison.CurrentCultureIgnoreCase;

            while (true)
            {
                int i = work.IndexOf(value1, start, caseInsensitive);
                if (i < 0) break;

                work = work.Left(i) + value2 + work.Right(work.Length - i - value1.Length);

                start += value2.Length;
            }

            return work;
        }

        public static string Right(this string text, int maxLength)
        {
            if (maxLength <= 0)
                return "";

            int start = Math.Max(text.Length - maxLength, 0);
            int count = Math.Min(maxLength, text.Length - start);
            
            return text.Substring(start, count);
        }

        public static bool StartsWithIgnoreCase(this string text, string value)
        {
            return text.StartsWith(value, System.StringComparison.CurrentCultureIgnoreCase);
        }

        public static string StripNonNumeric(this string text)
        {
            return text.StripNonNumeric(false);
        }

        public static string StripNonNumeric(this string text, bool stopAtFirst)
        {
            string result = "";

            foreach (var c in text)
            {
                if ((c.CompareTo('0') >= 0) && (c.CompareTo('9') <= 0))
                    result += c;
                else
                {
                    if (stopAtFirst)
                        break;
                }
            }

            return result;
        }

        public static byte[] ToByteArray(this string text)
        {
            int count = text.Length;
            byte[] result = new byte[count];

            for (int i = 0; i < count; i++)
                result[i] = (byte)text[i];

            return result;
        }

        public static string ToCsv(this string text)
        {
            // Converts comma-delimited or space-delimited to comma-delimited.
            string[] fields;

            if (text.Contains(",") || !text.Contains(" "))
                return text;

            // 'text' contains one or more spaces.
            string result = "";
            fields = text.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

            foreach (string field in fields)
            {
                if (result != "")
                    result += ",";

                result += field.Trim();
            }

            return result;
        }

        public static int[] ToIntArray(this string text)
        {
            return Array.ConvertAll<string, int>(text.ToCsv().ToStringArray(), int.Parse);
        }

        public static List<int> ToListInt(this string text)
        {
            return text.ToIntArray().ToList<int>();
        }

        public static string[] ToStringArray(this string text)
        {
            string[] result = text.ToCsv().Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

            for (int i = 0; i < result.Count(); i++)
                result[i] = result[i].Trim();

            return result;
        }

        public static List<string> ToListString(this string text)
        {
            return text.ToStringArray().ToList<string>();
        }
    }
}

C# - String Extensions - AddMarkup

Usage


string foo = "I got this from Just Snippets!";
string bar = foo.AddMarkup("Just Snippets", "", "");

would set bar to "I got this from Just Snippets!".

Snippet


namespace My.Helpers
{
    // String extension methods.

    public static class StringExtensions
    {
        public static string AddMarkup(this string text, string value, string before, string after, bool ignoreCase = true)
        {
            string work = text;
            int start = 0;
            System.StringComparison caseSensitivity;
            int bump = value.Length + before.Length + after.Length;

            if (ignoreCase)
                caseSensitivity = System.StringComparison.CurrentCultureIgnoreCase;
            else
                caseSensitivity = System.StringComparison.CurrentCulture;

            while (true)
            {
                int i = work.IndexOf(value, start, caseSensitivity);
                if (i < 0) break;

                work = work.Left(i) + before + work.Substring(i, value.Length) + after
                    + work.Right(work.Length - i - value.Length);

                start += bump;
            }

            return work;
        }

C# - Byte Array Extensions - ToHexString

using System.Text;

namespace My.Helpers
{
    public static class ByteExtensions
    {
        public static string ToHexString(this byte[] array)
        {
            // Create a new StringBuilder to collect the bytes and create a string.
            var sBuilder = new StringBuilder();

            // Convert to a hex string.
            for (int i = 0; i < array.Length; i++)
                sBuilder.Append(array[i].ToString("x2"));

            // Return the hex string.
            return sBuilder.ToString();
        }
    }
}

ASP.NET MVC - Admin Helpers

A small helper to add or remove a role from an ASP.NET user.

using System.Web.Security;

namespace My.Helpers
{
    public class AdminHelpers
    {
        public static bool SetRole(string userName, string roleName, bool newState)
        {
            bool currState = Roles.IsUserInRole(userName, roleName);

            if (newState != currState)
            {
                if (newState)
                {
                    // Add the 'roleName' role to this user.
                    Roles.AddUserToRole(userName, roleName);
                }
                else
                {
                    // Remove the 'roleName' role from this user.
                    Roles.RemoveUserFromRole(userName, roleName);
                }
            }

            return true;
        }
    }
}

ASP.NET MVC Mini-Profiler - Linq to SQL

This is a useful free profiler for ASP.NET MVC from the people at StackOverflow.com.

Some advice re. usage is in my previous post.

This post shows one way to use it in ASP.NET MVC with Linq to SQL.


In A DbRepository


I'm assuming a repository-based approach similar to NerdDinner.

I usually let my repository classes share a 'DataContext' - each controller typically has a main repository which creates the data context as usual, but other repositories needed in the controller actions are created using the second constructor below, and therefore use the first repository's data context.

    public class MyDbRepository : IMyRepository
    {
        public MyDataContext DataContext { get; protected set; }

        // Constructors.

        public MyDbRepository()
        {
            MiniProfiler profiler = MiniProfiler.Current;

            // Grab the SQL connection string (or could get it from web.config, for example).
            DataContext = new MyDataContext();
            string connString = DataContext.Connection.ConnectionString;

            // Setup a profiled connection.
            var conn = new SqlConnection(connString);
            var profiledConn = MvcMiniProfiler.Data.ProfiledDbConnection.Get(conn, profiler);

            // Create another data context using the profiled connection.
            DataContext = new MyDataContext(profiledConn);
        }

        public MyDbRepository(IMyRepository repos)
        {
            // Repositories can share a data context object.
            DataContext = repos.DataContext;
        }

        // Public methods.

        // +++

    }

Wednesday, 29 June 2011

ASP.NET MVC Mini-Profiler - First Impressions

This is a useful free profiler for ASP.NET MVC from the people at StackOverflow.com (a wonderful source of answers, and built using ASP.NET MVC).

The easiest way to install it in VS2010 is using NuGet

These snippets show one way to use it.

Site.Master (.ASPX view engine)

<script src="<%: Url.Content("https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js") %>"  type="text/javascript"></script>
<%= MvcMiniProfiler.MiniProfiler.RenderIncludes() %> 

Global.asax.cs

A simple approach is to start and stop the profiler for each separate web request, for example:

using MvcMiniProfiler; 

    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_BeginRequest()
        {
            if (Request.IsLocal)
                MiniProfiler.Start();
        }
        
        protected void Application_EndRequest()
        {
            if (Request.IsLocal)
                MiniProfiler.Stop();
        }
    }

In A Controller, for example


Create a separate MiniProfiler 'step' for any long processing steps that you want to profile:

var profiler = MiniProfiler.Current;
 
    // No need to check 'profiler' for null here, it's handled in profiler.Step()

    using (profiler.Step("Search"))
    {
        // Time-consuming stuff...
        DoLongSearch(xxx);
    }

Steps can be nested, and the profile output will show the step structure:

    using (profiler.Step("SearchAndUpdate"))
    {
        using (profiler.Step("Search"))
        {
            // Time-consuming stuff...
            DoSearch(xxx);
        }

        using (profiler.Step("Update"))
        {
            // Time-consuming stuff...
            DoUpdate(xxx);
        }
    }

Viewing the Output


When you navigate to a URL that causes the profiler to be called, the resulting view will contain a small MiniProfiler gadget at the top left. Click on it to get the timing detail.

Linq to SQL


You can use the MiniProfiler to profile database calls, including Linq to SQL - see this post. The profile information includes the elapsed time of each query and also warns of 'duplicate' SQL queries, both of which can help in optimizing application design.