Conditional Validation in ASP.NET MVC 3

I’m currently working on a project that has various forms where some fields are only required if another field has a certain value or is blank.

For example, it is a common requirement that a user must enter a value for either phone OR mobile. You can’t put a Required attribute on either of them as that would not solve the requirement.

Using the library developed by Simon Ince you could define it like this in your model;

[RequiredIf("Mobile", "", ErrorMessage="Please enter a value in either Phone or Mobile")]
 public string Phone { get; set; }

[RequiredIf("Phone", "", ErrorMessage = "Please enter a value in either Mobile or Phone")]
 public string Mobile { get; set; }

Here are some relevant links to add custom validation logic, both client and server side, to your MVC projects:

http://blogs.msdn.com/b/simonince/archive/2011/09/29/mvc-validationtookit-alpha-release-conditional-validation-with-mvc-3.aspx

http://blogs.msdn.com/b/stuartleeks/archive/2010/08/06/asp-net-mvc-adding-client-side-validation-to-propertiesmustmatchattribute.aspx

http://foolproof.codeplex.com/

Tagged with: , ,
Posted in Uncategorized

Repeating background tile with a specific height

The other day I was working on a site that used curvy lines to surround content on the site instead of the more usual straight-line boxes around content.

Up until that point the content on the site was fairly static.

However the new addition to the site was a blog and so the blog articles would be of variable length and so too were the number of comments for every article.

So the challenge was to ensure the curvy lines always connected up at the bottom of the container.

Here is a quick peak at the design:

curvy-lines

Here is the html code

<div class="blog-box-container">
 <div class="blog-box-container-top"></div>
 <div class="blog-box-container-mid" style="min-height: 450px">
 <div class="blog-box-container-padding">
 <h1>Here is a heading</h1>
 <p>Here is some content</p>
 </div>
 </div>
 <div class="blog-box-container-bottom"></div>
</div>

Here is the css:

.blog-box-container{
 width: 847px;
 margin: 22px auto 0 auto;
}

.blog-box-container-top{
 background: url('img/top.png') no-repeat;
 height: 82px;
}

/* Use the "resizeBoxes" jquery script
 to ensure all elements with this class are multiples of
 the height of the background image used */
.blog-box-container-mid{
 background: url('img/middle.png') repeat-y;
}

.blog-box-container-bottom{
 background: url('img/bottom.png') no-repeat;
 height: 22px;
}

.blog-box-container-padding{
 padding: 1px 15px;
 position: relative;
}

To solve this problem I used a JavaScript function to ensure the middle panel always had a height that was a multiple of the tile background image to be used.

Here is the JavaScript:

// This function will ensure all matched elements have a height
// that is a multiple of the ideal height specified.
// If we have a repeating tiled background of a specific height
// this will ensure the full tile is shown.
function resizeBoxes(selector, idealHeight) {
 var elements = $(selector).each(function () {

$(this).css("height", "auto"); // need to clear any preset height on the element first

var height = $(this).height();
 var remainder = height % idealHeight;

if (remainder != 0) {
 var divisor = parseInt(height / idealHeight) + 1;
 var newHeight = divisor * idealHeight;
 $(this).height(newHeight);
 }
 });
}

So to use the JavaScript function you just call it like this on the page:


$(function () {
 resizeBoxes(".blog-box-container-mid", 172);
 });

I hope this code helps someone else out who faces a similar problem.

Tagged with: ,
Posted in Layout

MVC Logging Part 6 – Controller / Views

This is part 6 of the MVC Logging series. Other articles in the series are:

Introduction

In the previous article we set up our model and the repositories that we need to fetch the data from the database. Now we need to create the necessary controllers and views so that we can present all of the consolidated logging messages on our log reporting dashboard page.

Creating our Logging Controller

The first thing we need to do is create a controller for our log reporting pages. Here are the steps to follow:

1. In the solution explorer, right-click the “Controllers” directory and select “Add -> Controller”.

2. Type in the name “LoggingController”, leave the check-box unticked and click “Add”.

At this point let’s take a minute to think about what we want to display on our Logging landing page.

We would like to:

* Display a filterable grid of all the log messages. The user should be able to filter by date, the name of the log provider (eg. NLog, Elmah etc), and the log level (eg. Debug, Info, Error etc)
* Allow the user to page through the results
* Allow the user to choose how many records to display per page.
* Allow the user to click on a row in the grid to view more detailed information about the log message.

OK, great! So it looks like we will need the following parameters for our view:

* Date Start
* Date End
* Log Provider Name
* Log Level
* Current Page Index
* Page Size

Let’s create a ViewModel called “LoggingIndexModel” to store all of these details. We’ll also combine the Start and End Dates into a string based representation called Period which will contain values like “Today”, Yesterday”, “Last Week” etc.

1. Create a new directory in the root of your website and name it “ViewModels”.

2. Add a new file called, “LoggingIndexModel.cs”.

3. Add the following code into the class:

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

using MvcLoggingDemo.Models;
using MvcLoggingDemo.Services.Paging;

namespace MvcLoggingDemo.ViewModels
{
 public class LoggingIndexModel
 {
 public IPagedList<LogEvent> LogEvents { get; set; }

 public string LoggerProviderName { get; set; }
 public string LogLevel { get; set; }
 public string Period { get; set; }

 public int CurrentPageIndex { get; set; }
 public int PageSize { get; set; }

 public LoggingIndexModel()
 {
 CurrentPageIndex = 0;
 PageSize = 20;
 }
 }
}

Go ahead and modify our controller class so that it looks like the following :


using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Web;
using System.Web.Mvc;

using MvcLoggingDemo.Helpers;
using MvcLoggingDemo.Models.Repository;
using MvcLoggingDemo.Models;
using MvcLoggingDemo.ViewModels;

using MvcLoggingDemo.Services.Paging;

namespace MySampleApp.Controllers
{
 [Authorize]
 public class LoggingController : Controller
 {
 private readonly ILogReportingFacade loggingRepository;

 public LoggingController()
 {
 loggingRepository = new LogReportingFacade();
 }

 public LoggingController(ILogReportingFacade repository)
 {
 loggingRepository = repository;
 }

 /// <summary>
 /// Returns the Index view
 /// </summary>
 /// <param name="Period">Text representation of the date time period. eg: Today, Yesterday, Last Week etc.</param>
 /// <param name="LoggerProviderName">Elmah, Log4Net, NLog, Health Monitoring etc</param>
 /// <param name="LogLevel">Debug, Info, Warning, Error, Fatal</param>
 /// <param name="page">The current page index (0 based)</param>
 /// <param name="PageSize">The number of records per page</param>
 /// <returns></returns>
 public ActionResult Index(string Period, string LoggerProviderName, string LogLevel, int? page, int? PageSize)
 {
 // Set up our default values
 string defaultPeriod = Session["Period"] == null ? "Today" : Session["Period"].ToString();
 string defaultLogType = Session["LoggerProviderName"] == null ? "All" : Session["LoggerProviderName"].ToString();
 string defaultLogLevel = Session["LogLevel"] == null ? "Error" : Session["LogLevel"].ToString();

 // Set up our view model
 LoggingIndexModel model = new LoggingIndexModel();

 model.Period = (Period == null) ? defaultPeriod : Period;
 model.LoggerProviderName = (LoggerProviderName == null) ? defaultLogType : LoggerProviderName;
 model.LogLevel = (LogLevel == null) ? defaultLogLevel : LogLevel;
 model.CurrentPageIndex = page.HasValue ? page.Value - 1 : 0;
 model.PageSize = PageSize.HasValue ? PageSize.Value : 20;

 TimePeriod timePeriod = TimePeriodHelper.GetUtcTimePeriod(model.Period);

 // Grab the data from the database
 model.LogEvents = loggingRepository.GetByDateRangeAndType(model.CurrentPageIndex, model.PageSize, timePeriod.Start, timePeriod.End, model.LoggerProviderName, model.LogLevel);

 // Put this into the ViewModel so our Pager can get at these values
 ViewData["Period"] = model.Period;
 ViewData["LoggerProviderName"] = model.LoggerProviderName;
 ViewData["LogLevel"] = model.LogLevel;
 ViewData["PageSize"] = model.PageSize;

 // Put the info into the Session so that when we browse away from the page and come back that the last settings are rememberd and used.
 Session["Period"] = model.Period;
 Session["LoggerProviderName"] = model.LoggerProviderName;
 Session["LogLevel"] = model.LogLevel;

 return View(model);
 }
}
}

Index View

Now right-click the Index method in the Controller and Add the Index View.

Choose the “Strongly typed view” option and select “MvcLoggingDemo.ViewModels.LoggingIndexModel” as the Type.

Select Empty for “View Type” and click “OK” to create a blank slate for our Index view.

Our Dashboard will allow users to see a list of errors, a chart of the errors and also a RSS feed of the errors so let’s add those options to the top of our Index page:

<div>
 View :
 <strong>List</strong>
 | <%: Html.ActionLink("Chart", "Chart")%>
 | <%: Html.ActionLink("RSS", "RssFeed", new { LoggerProviderName = Model.LoggerProviderName, Period = Model.Period, LogLevel = Model.LogLevel }, new { target = "_blank" })%>
 </div>

As our index page will be the list or grid based view we will need a way to filter the error messages to be displayed. So let’s start a HTML form and add some filtering fields to our view:

<div>
 <div>

 Logger : <%: Html.DropDownList("LoggerProviderName", new SelectList(MvcLoggingDemo.Helpers.FormsHelper.LogProviderNames, "Value", "Text"))%>

 Level : <%: Html.DropDownList("LogLevel", new SelectList(MvcLoggingDemo.Helpers.FormsHelper.LogLevels, "Value", "Text"))%>

 For : <%: Html.DropDownList("Period", new SelectList(MvcLoggingDemo.Helpers.FormsHelper.CommonTimePeriods, "Value", "Text"))%>

 <input id="btnGo" name="btnGo" type="submit" value="Apply Filter" />

 </div>
 </div>

We also need a header for our grid that will display the number of  messages found  and a way for the user to change the number of records displayed per page. Let’s add our grid header now:

<div>

 <div>
 <div>

 <span style="float: left">
 <%: string.Format("{0} records found. Page {1} of {2}", Model.LogEvents.TotalItemCount, Model.LogEvents.PageNumber, Model.LogEvents.PageCount)%>
 </span>

 <span style="float: right">
 Show <%: Html.DropDownList("PageSize", new SelectList(MvcLoggingDemo.Helpers.FormsHelper.PagingPageSizes, "Value", "Text"), new { onchange = "document.getElementById('myform').submit()" })%> results per page
 </span>

 <div style="clear: both"></div>

 </div>

 </div>

 <div>
 <div>
 <%= Html.Pager(ViewData.Model.LogEvents.PageSize, ViewData.Model.LogEvents.PageNumber, ViewData.Model.LogEvents.TotalItemCount, new { LogType = ViewData["LogType"], Period = ViewData["Period"], PageSize = ViewData["PageSize"] })%>
 </div>
 </div>

 </div>

 <% } %>

Notice how the paging helper using the information from the ViewData and also our need to add in the routing data to include information from our filters.

With the grid filter and the grid header all done we now need to turn our attention to the actual grid data. Add the following code to the view:

<% if (Model.LogEvents.Count() == 0) { %>

 <p>No results found</p>

 <% } else { %>

 <div>
 <table>
 <tr>
 <th></th>
 <th>
 #
 </th>
 <th>
 Log
 </th>
 <th>
 Date
 </th>
 <th style='white-space: nowrap;'>
 Time ago
 </th>
 <th>
 Host
 </th>
 <th>
 Source
 </th>
 <th>
 Message
 </th>
 <th>
 Type
 </th>
 <th>
 Level
 </th>
 </tr>

 <% int i = 0;  foreach (var item in Model.LogEvents)
 { %>

 <tralt" : "" %>">
 <td>
 <%: Html.ActionLink("Details", "Details", new { id = item.Id.ToString(), loggerProviderName = item.LoggerProviderName })%>
 </td>
 <td>
 <%: i.ToString() %>
 </td>
 <td>
 <%: item.LoggerProviderName%>
 </td>
 <td style='white-space: nowrap;'>
 <%: String.Format("{0:g}", item.LogDate.ToLocalTime())%>
 </td>
 <td style='white-space: nowrap;'>
 <%: item.LogDate.ToLocalTime().TimeAgoString()%>
 </td>
 <td>
 <%: item.MachineName%>
 </td>
 <td>
 <%: item.Source%>
 </td>
 <td>
 <pre><%: item.Message.WordWrap(80) %></pre>
 </td>
 <td>
 <%: item.Type%>
 </td>
 <td>
 <%: item.Level.ToLower().ToPascalCase() %>
 </td>
 </tr>

 <% } %>

 </table>
 </div>

 <% } %>

 <div>

 <div>
 <div>
 <%= Html.Pager(ViewData.Model.LogEvents.PageSize, ViewData.Model.LogEvents.PageNumber, ViewData.Model.LogEvents.TotalItemCount, new { LogType = ViewData["LogType"], Period = ViewData["Period"], PageSize = ViewData["PageSize"] })%>
 </div>
 </div>

 </div>

In the snippet above, first we check to see if there are any log messages to be displayed. If not we let the user know that no records were found.

Instead of just displaying the datetime that the log message was recorded we also add a column to display the amount of time that has elapsed since then in a user friendly way. We use a DateTime extension method called “TimeAgoString()” to accomplish this.

Another nice feature is that we would also like to word wrap the error messages. Whilst building this project I received several error messages that had very long sequences of unbroken characters. The WordWrap helper function will split very long strings into lines that have a maximum length that you specify. It will try to break the lines apart on words but if it encounters long unbroken text it will just split them where necessary. The end result is a nicely formatted display.

Also, when dealing with multiple log providers it is nice to display the Log Level consistently across all of them. In order to accomplish this we convert the Log Level to lower case and then to PascalCase using a string extension helper method.

And lastly, we repeat the paging at the bottom of the grid so that if the user is looking at 50 or 100 records at a time they do not need to scroll back up to the top of the grid to navigate to the next page of results.

Now let’s run the website, log in, and view our Log Reporting Dashboard page:

To wrap up this article let’s quickly create the details page for a log message.

Details View

1. The first step is to go to the “Logging” controller and add the following code underneath the “Index” action:

//
 // GET: /Logging/Details/5

 public ActionResult Details(string loggerProviderName, string id)
 {
 LogEvent logEvent = loggingRepository.GetById(loggerProviderName, id);

 return View(logEvent);
 }

2. Right-click the “Details” method in the “Controller” and choose “Add View”
Select “Strongly Typed View” and choose the “MvcLoggingDemo.Models.LogEvent” Type.
Select “Empty” for the “Content Type”
Click on “Add”

3. Add the following code to the “Details” page to the “MainContent” content placeholder:

<h2>Details</h2>

 <p>
 <%: Html.ActionLink("Back to List", "Index") %>
 </p>

 <fieldset>
 <legend>Fields</legend>

 <div>Id</div>
 <div><%: Model.Id %></div>

 <div>LogDate</div>
 <div><%: String.Format("{0:g}", Model.LogDate) %></div>

 <div>Name</div>
 <div><%: Model.LoggerProviderName %></div>

 <div>Source</div>
 <div><%: Model.Source %></div>

 <div>MachineName</div>
 <div><%: Model.MachineName %></div>

 <div>Type</div>
 <div><%: Model.Type %></div>

 <div>Level</div>
 <div><%: Model.Level %></div>

 <div>Message</div>
 <div>
 <pre><%: Model.Message.WordWrap(80) %></pre>
 </div>

 <div>StackTrace</div>
 <div><%: Model.StackTrace %></div>

 </fieldset>

 <% =FormsHelper.OutputXmlTableForLogging(Model.AllXml) %>

 <p>
 <%: Html.ActionLink("Back to List", "Index") %>
 </p>

Notice that we once again we use the WordWrap helper function to tidy up the error message

At the bottom of the page we use another helper method to display the server variables and cookies in a table.

Here is what the details page looks like :

Conclusion

That ends part 6 of this series. In this article we created the Logging controller and views for the “Index” and “Details” actions on the controller. In the next article we will add an RSS feed to our dashboard.

Download

The sourcecode for part 6 is on the Downloads tab of the associated Codeplex website

Tagged with: , , , ,
Posted in ASP.NET MVC

Logging in MVC Part 5 – The Model and Data Layer

This is part 5 of the MVC Logging series. Other articles in the series are:

Introduction

This is the 5th article in a series on MVC logging.

Our sample website now has Elmah, NLog, Log4Net and Health monitoring set up and working. Now it is time to start work on our log reporting website by tying them all together.

Preparing the database

ASP.NET Health Monitoring logs a lot of different types of messages but there is no way to differentiate whether a message is just for information purposes or whether it is an error message that may need attention. So to address this issue, let’s create a new table called “aspnet_WebEvent_ErrorCodes” and introduce a column called “Level” which will map each message eventcode to either “Info”, “Error”.

The reason for doing this is so that we have a common “Level” attribute that we can use for all of our logging providers and this will allow us to later on filter all messages by their Log Level. Elmah for example will only be used to log unhandled exceptions so the LogLevel for our Elmah messages will always be “Error”.

Here is the database script necessary to add the new table to our database:

/****** Object:  Table [dbo].[aspnet_WebEvent_ErrorCodes]    Script Date: 07/29/2010 09:56:45 ******/
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[aspnet_WebEvent_ErrorCodes]') AND type in (N'U'))
DROP TABLE [dbo].[aspnet_WebEvent_ErrorCodes]
GO
/****** Object:  Default [DF_aspnet_WebEvent_ErrorCodes_Level]    Script Date: 07/29/2010 09:56:45 ******/
IF  EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_aspnet_WebEvent_ErrorCodes_Level]') AND parent_object_id = OBJECT_ID(N'[dbo].[aspnet_WebEvent_ErrorCodes]'))
Begin
IF  EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_aspnet_WebEvent_ErrorCodes_Level]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[aspnet_WebEvent_ErrorCodes] DROP CONSTRAINT [DF_aspnet_WebEvent_ErrorCodes_Level]
END

End
GO
/****** Object:  Table [dbo].[aspnet_WebEvent_ErrorCodes]    Script Date: 07/29/2010 09:56:45 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[aspnet_WebEvent_ErrorCodes]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[aspnet_WebEvent_ErrorCodes](
 [Id] [int] IDENTITY(1,1) NOT NULL,
 [Name] [nvarchar](255) COLLATE Latin1_General_CI_AS NOT NULL,
 [EventCode] [int] NOT NULL,
 [Level] [nvarchar](10) COLLATE Latin1_General_CI_AS NOT NULL,
 CONSTRAINT [PK_aspnet_WebEvent_ErrorCodes] PRIMARY KEY CLUSTERED
(
 [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)
)
END
GO
SET IDENTITY_INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ON
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (1, N'InvalidEventCode', -1, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (2, N'UndefinedEventCode/UndefinedEventDetailCode', 0, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (3, N'Not used', -9999, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (4, N'ApplicationCodeBase', 1000, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (5, N'ApplicationStart', 1001, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (6, N'ApplicationShutdown', 1002, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (7, N'ApplicationCompilationStart', 1003, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (8, N'ApplicationCompilationEnd', 1004, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (9, N'ApplicationHeartbeat', 1005, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (10, N'RequestCodeBase', 2000, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (11, N'RequestTransactionComplete', 2001, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (12, N'RequestTransactionAbort', 2002, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (13, N'ErrorCodeBase', 3000, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (14, N'RuntimeErrorRequestAbort', 3001, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (15, N'RuntimeErrorViewStateFailure', 3002, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (16, N'RuntimeErrorValidationFailure', 3003, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (17, N'RuntimeErrorPostTooLarge', 3004, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (18, N'RuntimeErrorUnhandledException', 3005, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (19, N'WebErrorParserError', 3006, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (20, N'WebErrorCompilationError', 3007, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (21, N'WebErrorConfigurationError', 3008, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (22, N'WebErrorOtherError', 3009, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (23, N'WebErrorPropertyDeserializationError', 3010, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (24, N'WebErrorObjectStateFormatterDeserializationError', 3011, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (25, N'AuditCodeBase', 4000, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (26, N'AuditFormsAuthenticationSuccess', 4001, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (27, N'AuditMembershipAuthenticationSuccess', 4002, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (28, N'AuditUrlAuthorizationSuccess', 4003, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (29, N'AuditFileAuthorizationSuccess', 4004, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (30, N'AuditFormsAuthenticationFailure', 4005, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (31, N'AuditMembershipAuthenticationFailure', 4006, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (32, N'AuditUrlAuthorizationFailure', 4007, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (33, N'AuditFileAuthorizationFailure', 4008, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (34, N'AuditInvalidViewStateFailure', 4009, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (35, N'AuditUnhandledSecurityException', 4010, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (36, N'AuditUnhandledAccessException', 4011, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (37, N'MiscCodeBase', 6000, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (38, N'WebEventProviderInformation', 6001, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (39, N'ApplicationDetailCodeBase', 50000, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (40, N'ApplicationShutdownUnknown', 50001, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (41, N'ApplicationShutdownHostingEnvironment', 50002, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (42, N'ApplicationShutdownChangeInGlobalAsax', 50003, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (43, N'ApplicationShutdownConfigurationChange', 50004, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (44, N'ApplicationShutdownUnloadAppDomainCalled', 50005, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (45, N'ApplicationShutdownChangeInSecurityPolicyFile', 50006, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (46, N'ApplicationShutdownBinDirChangeOrDirectoryRename', 50007, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (47, N'ApplicationShutdownBrowsersDirChangeOrDirectoryRename', 50008, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (48, N'ApplicationShutdownCodeDirChangeOrDirectoryRename', 50009, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (49, N'ApplicationShutdownResourcesDirChangeOrDirectoryRename', 50010, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (50, N'ApplicationShutdownIdleTimeout', 50011, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (51, N'ApplicationShutdownPhysicalApplicationPathChanged', 50012, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (52, N'ApplicationShutdownHttpRuntimeClose', 50013, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (53, N'ApplicationShutdownInitializationError', 50014, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (54, N'ApplicationShutdownMaxRecompilationsReached', 50015, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (55, N'StateServerConnectionError', 50016, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (56, N'AuditDetailCodeBase', 50200, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (57, N'InvalidTicketFailure', 50201, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (58, N'ExpiredTicketFailure', 50202, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (59, N'InvalidViewStateMac', 50203, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (60, N'InvalidViewState', 50204, N'Error')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (61, N'WebEventDetailCodeBase', 50300, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (62, N'SqlProviderEventsDropped', 50301, N'Info')
INSERT [dbo].[aspnet_WebEvent_ErrorCodes] ([Id], [Name], [EventCode], [Level]) VALUES (63, N'WebExtendedBase', 100000, N'Info')
SET IDENTITY_INSERT [dbo].[aspnet_WebEvent_ErrorCodes] OFF
/****** Object:  Default [DF_aspnet_WebEvent_ErrorCodes_Level]    Script Date: 07/29/2010 09:56:45 ******/
IF Not EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_aspnet_WebEvent_ErrorCodes_Level]') AND parent_object_id = OBJECT_ID(N'[dbo].[aspnet_WebEvent_ErrorCodes]'))
Begin
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_aspnet_WebEvent_ErrorCodes_Level]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[aspnet_WebEvent_ErrorCodes] ADD  CONSTRAINT [DF_aspnet_WebEvent_ErrorCodes_Level]  DEFAULT ('Info') FOR [Level]
END

End
GO

The last step in preparing our database is to create a new view that will return all of the ASP.NET Health Monitoring messages with the LogLevel included.

Here is the database script to create the view:


CREATE VIEW vw_aspnet_WebEvents_extended
AS

SELECT
 webEvent.EventId
 , webEvent.EventTimeUtc
 , webEvent.EventTime
 , webEvent.EventType
 , webEvent.EventSequence
 , webEvent.EventOccurrence
 , webEvent.EventCode
 , webEvent.EventDetailCode
 , webEvent.Message
 , webEvent.ApplicationPath
 , webEvent.ApplicationVirtualPath
 , webEvent.MachineName
 , webEvent.RequestUrl
 , webEvent.ExceptionType
 , webEvent.Details
 , webEventCodes.Level
FROM
 dbo.aspnet_WebEvent_Events AS webEvent
INNER JOIN
 dbo.aspnet_WebEvent_ErrorCodes AS webEventCodes ON webEvent.EventCode = webEventCodes.EventCode

Before we get started on our model and data access layer I want to introduce some helper classes that we will be using along the way.

Paging Service

Every database application needs to page results from the database and display them to the end user.I looked at several paging implementations on the web but finally settled on Martin Bolands paging mechanism.

In our sample website I have created a new folder called “Paging” under the “Services” folder and added the following 4 classes (code is provided in the downloadable code at the end of this article) :

IPagedList.cs
PagedList.cs
Pager.cs
PagingExtensions.cs

We will be making use of the PagedList class in our repository class below.

Creating the Model

For our sample website we will be using Linq to Entities.

1. Create a new folder underneath Models and name it “Entities”.
2. Right-click the folder and select “Add -> New Item”.
3. Select the “ADO.NET Entity Data Model” from the Data category. In the name field, type “MvcLoggingDemo.edmx” and click on “Add”.
4. Select the “Create from Database” option and the click on “Next”.
5. The wizard should automatically find the SampleDatabase.mdf in the app_data folder. Ensure that the checkbox at the bottom of the dialog is “checked” and provide the connection string name of  “MvcLoggingDemoContainer”. Click “Next”.
6. Expand the tables node and select the following tables : Elmah_Error, NLog_Error, Log4Net_Error


7. Expand the views node and select the following view : vw_aspnet_WebEvents_extended
8. Click OK and the tables and the view should now be added to the Entities diagram.

At this point Linq to Entities will have created 4 entities that we can use to retrieve information from our database but we need a new Entity that we can use to store the common information from all of our tables.

So underneath our Models folder, create a new class and name it “LogEvent.cs”

Replace the file contents with the code below:

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

namespace MvcLoggingDemo.Models
{

 /// <summary>
 /// This represents a generic log message that can store log information about
 /// any logger implemented. Eg: Log4Net, NLog, Health Monitoring, Elmah
 /// </summary>
 public class LogEvent
 {
 private string _Id = string.Empty;

 /// <summary>
 /// String representation of the event log id
 /// </summary>
 public string Id
 {
 get
 {
 switch (IdType)
 {
 case "number":
 return IdAsInteger.ToString();

 case "guid":
 return IdAsGuid.ToString();

 default:
 return _Id;
 }
 }

 set
 {
 _Id = value;
 }
 }

 /// <summary>
 /// Stores the Id of the log event as a GUID
 /// </summary>
 internal Guid IdAsGuid { get; set; }

 /// <summary>
 /// Stores the Id of the log event as an integer
 /// </summary>
 internal int IdAsInteger { get; set; }

 /// <summary>
 /// Stores the base type of the id
 /// Valid values are : number, guid, string
 /// </summary>
 internal string IdType { get; set; }

 /// <summary>
 /// The date of the log event
 /// </summary>
 public DateTime LogDate { get; set; }

 /// <summary>
 /// The name of the log provider
 /// Example values are NLog, Log4Net, Elmah, Health Monitoring
 /// </summary>
 public string LoggerProviderName { get; set; }

 /// <summary>
 /// Information about where the error occurred
 /// </summary>
 public string Source { get; set; }

 /// <summary>
 /// The machine where the error occured
 /// </summary>
 public string MachineName { get; set; }

 /// <summary>
 /// The Type name of the class that logged the error
 /// </summary>
 public string Type { get; set; }

 /// <summary>
 /// The level of the message logged
 /// Valid values are : Debug, Info, Warning, Error, Fatal
 /// </summary>
 public string Level { get; set; }

 /// <summary>
 /// The message that was logged
 /// </summary>
 public string Message { get; set; }

 /// <summary>
 /// If the message was from an error this value will contain details of the stack trace.
 /// Otherwise it will be empty
 /// </summary>
 public string StackTrace { get; set; }

 /// <summary>
 /// If the message was from an error this value will contain details of the HTTP Server variables and Cookies.
 /// Otherwise it will be empty
 /// </summary>
 public string AllXml { get; set; }
 }
}

Our various logging providers all use a different kind of primary key. Some use a unique identifier, some use an integer. So our LogEvent class has a few properties that can store the Id in various datatypes internally within our assembly but we have one public Id property available to all the consumers of the class. To accommodate this we also introduce another property called IdType that stores the type of datatype used by the logging provider. At first I tried to use a generic “Object” datatype for the Id property but I could not get the Linq to Entities query working when doing a UNION on all of the IQueryable results returned from the providers.

All of the other public properties are self-explanatory.

Creating the Data Access Layer

For our Data Access Layer we will be using the Repository pattern and the first thing we need to do is create an interface that our repository will implement.

1. Create a new folder called Repository underneath Models
2. Create a new folder underneath Repository called Interfaces
3. In the Interfaces directory, create a new class file and name it, “ILogReportingRepository”
4. Replace the contents of the newly created file with the code below :

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

using MvcLoggingDemo.Services.Paging;

namespace MvcLoggingDemo.Models.Repository
{
 /// <summary>
 /// This interface provides the methods that we need to so that we can report on log messages stored by the various
 /// logging tools used in our website
 /// </summary>
 public interface ILogReportingRepository
 {
 /// <summary>
 /// Gets a filtered list of log events
 /// </summary>
 /// <param name="pageIndex">0 based page index</param>
 /// <param name="pageSize">max number of records to return</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevel">The level of the log messages</param>
 /// <returns>A filtered list of log events</returns>
 IQueryable<LogEvent> GetByDateRangeAndType(int pageIndex, int pageSize, DateTime start, DateTime end, string logLevel);

 /// <summary>
 /// Returns a single Log event
 /// </summary>
 /// <param name="id">Id of the log event as a string</param>
 /// <returns>A single Log event</returns>
 LogEvent GetById(string id);

 /// <summary>
 /// Clears log messages between a date range and for specified log levels
 /// </summary>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevels">string array of log levels</param>
 void ClearLog(DateTime start, DateTime end, string[] logLevels);
 }
}

5. In the Repository directory, create a new class file and name it, “ElmahRepository”
6. Replace the contents of the newly created file with the code below :

using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Data.SqlClient;
using System.Linq;
using System.Web;

using MvcLoggingDemo.Models.Entities;
using MvcLoggingDemo.Services.Paging;
using MvcLoggingDemo.Helpers;

namespace MvcLoggingDemo.Models.Repository
{
 /// <summary>
 /// This class extracts information that Elmah stores so that we can report on it
 /// </summary>
 public class ElmahRepository : ILogReportingRepository
 {
 MvcLoggingDemoContainer _context = null;

 /// <summary>
 /// Default Constructor uses the default Entity Container
 /// </summary>
 public ElmahRepository()
 {
 _context = new MvcLoggingDemoContainer();
 }

 /// <summary>
 /// Overloaded constructor that can take an EntityContainer as a parameter so that it can be mocked out by our tests
 /// </summary>
 /// <param name="context">The Entity context</param>
 public ElmahRepository(MvcLoggingDemoContainer context)
 {
 _context = context;
 }

 /// <summary>
 /// Gets a filtered list of log events
 /// </summary>
 /// <param name="pageIndex">0 based page index</param>
 /// <param name="pageSize">max number of records to return</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevel">The level of the log messages</param>
 /// <returns>A filtered list of log events</returns>
 public IQueryable<LogEvent> GetByDateRangeAndType(int pageIndex, int pageSize, DateTime start, DateTime end, string logLevel)
 {
 IQueryable<LogEvent> list = (from a in _context.ELMAH_Error
 where a.TimeUtc >= start && a.TimeUtc <= end
 && (logLevel == "All" || logLevel == "Error")
 select new LogEvent { IdType = "guid"
 , Id = ""
 , IdAsInteger = 0
 , IdAsGuid = a.ErrorId
 , LoggerProviderName = "Elmah"
 , LogDate = a.TimeUtc
 , MachineName = a.Host
 , Message = a.Message
 , Type = a.Type
 , Level = "Error"
 , Source = a.Source, StackTrace = "" });

 return list;
 }

 /// <summary>
 /// Returns a single Log event
 /// </summary>
 /// <param name="id">Id of the log event as a string</param>
 /// <returns>A single Log event</returns>
 public LogEvent GetById(string id)
 {
 Guid guid = new Guid(id);
 LogEvent logEvent = (from b in _context.ELMAH_Error
 where b.ErrorId == guid
 select new LogEvent { IdType = "guid"
 , IdAsGuid = b.ErrorId
 , LoggerProviderName = "Elmah"
 , LogDate = b.TimeUtc
 , MachineName = b.Host
 , Message = b.Message
 , Type = b.Type
 , Level = "Error"
 , Source = b.Source
 , StackTrace = ""
 , AllXml = b.AllXml })
 .SingleOrDefault();

 return logEvent;
 }

 /// <summary>
 /// Clears log messages between a date range and for specified log levels
 /// </summary>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevels">string array of log levels</param>
 public void ClearLog(DateTime start, DateTime end, string[] logLevels)
 {
 string commandText = "delete from Elmah_Error WHERE TimeUtc >= @p0 and TimeUtc <= @p1";

 SqlParameter paramStartDate = new SqlParameter { ParameterName = "p0", Value = start.ToUniversalTime(), DbType = System.Data.DbType.DateTime };
 SqlParameter paramEndDate = new SqlParameter { ParameterName = "p1", Value = end.ToUniversalTime(), DbType = System.Data.DbType.DateTime };

 _context.ExecuteStoreCommand(commandText, paramStartDate, paramEndDate);
 }

 }
}
 

7. In the Repository directory, create a new class file and name it, “NLogRepository”
8. Replace the contents of the newly created file with the code below :

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

using MvcLoggingDemo.Models.Entities;
using MvcLoggingDemo.Services.Paging;
using MvcLoggingDemo.Helpers;
using System.Data.SqlClient;

namespace MvcLoggingDemo.Models.Repository
{
 /// <summary>
 /// This class extracts information that NLog stores so that we can report on it
 /// </summary>
 public class NLogRepository : ILogReportingRepository
 {
 MvcLoggingDemoContainer _context = null;

 /// <summary>
 /// Default Constructor uses the default Entity Container
 /// </summary>
 public NLogRepository()
 {
 _context = new MvcLoggingDemoContainer();
 }

 /// <summary>
 /// Overloaded constructor that can take an EntityContainer as a parameter so that it can be mocked out by our tests
 /// </summary>
 /// <param name="context">The Entity context</param>
 public NLogRepository(MvcLoggingDemoContainer context)
 {
 _context = context;
 }

 /// <summary>
 /// Gets a filtered list of log events
 /// </summary>
 /// <param name="pageIndex">0 based page index</param>
 /// <param name="pageSize">max number of records to return</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevel">The level of the log messages</param>
 /// <returns>A filtered list of log events</returns>
 public IQueryable<LogEvent> GetByDateRangeAndType(int pageIndex, int pageSize, DateTime start, DateTime end, string logLevel)
 {
 IQueryable<LogEvent> list = (from b in _context.NLog_Error
 where b.time_stamp >= start && b.time_stamp <= end
 && (b.level == logLevel || logLevel == "All")
 select new LogEvent { IdType = "number"
 , Id = ""
 , IdAsInteger = b.Id
 , IdAsGuid = Guid.NewGuid()
 , LoggerProviderName = "NLog"
 , LogDate = b.time_stamp
 , MachineName = b.host
 , Message = b.message
 , Type = b.type
 , Level = b.level
 , Source = b.source
 , StackTrace = b.stacktrace });

 return list;
 }

 /// <summary>
 /// Returns a single Log event
 /// </summary>
 /// <param name="id">Id of the log event as a string</param>
 /// <returns>A single Log event</returns>
 public LogEvent GetById(string id)
 {
 int logEventId = Convert.ToInt32(id);

 LogEvent logEvent = (from b in _context.NLog_Error
 where b.Id == logEventId
 select new LogEvent { IdType = "number"
 , IdAsInteger = b.Id
 , LoggerProviderName = "NLog"
 , LogDate = b.time_stamp
 , MachineName = b.host
 , Message = b.message
 , Type = b.type
 , Level = b.level
 , Source = b.source
 , StackTrace = b.stacktrace
 , AllXml = b.allxml })
 .SingleOrDefault();

 return logEvent;

 }

 /// <summary>
 /// Clears log messages between a date range and for specified log levels
 /// </summary>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevels">string array of log levels</param>
 public void ClearLog(DateTime start, DateTime end, string[] logLevels)
 {
 string logLevelList = "";
 foreach (string logLevel in logLevels)
 {
 logLevelList += ",'" + logLevel + "'";
 }
 if (logLevelList.Length > 0)
 {
 logLevelList = logLevelList.Substring(1);
 }

 string commandText = "delete from NLog_Error WHERE time_stamp >= @p0 and time_stamp <= @p1 and level in (@p2)";

 SqlParameter paramStartDate = new SqlParameter { ParameterName = "p0", Value = start.ToUniversalTime(), DbType = System.Data.DbType.DateTime };
 SqlParameter paramEndDate = new SqlParameter { ParameterName = "p1", Value = end.ToUniversalTime(), DbType = System.Data.DbType.DateTime };
 SqlParameter paramLogLevelList = new SqlParameter { ParameterName = "p2", Value = logLevelList };

 _context.ExecuteStoreCommand(commandText, paramStartDate, paramEndDate, paramLogLevelList);
 }

 }
}
 

9. In the Repository directory, create a new class file and name it, “Log4NetRepository”
10. Replace the contents of the newly created file with the code below :

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

using MvcLoggingDemo.Models.Entities;
using MvcLoggingDemo.Services.Paging;
using MvcLoggingDemo.Helpers;
using System.Data.SqlClient;

namespace MvcLoggingDemo.Models.Repository
{
 /// <summary>
 /// This class extracts information that Log4Net stores so that we can report on it
 /// </summary>
 public class Log4NetRepository : ILogReportingRepository
 {
 MvcLoggingDemoContainer _context = null;

 /// <summary>
 /// Default Constructor uses the default Entity Container
 /// </summary>
 public Log4NetRepository()
 {
 _context = new MvcLoggingDemoContainer();
 }

 /// <summary>
 /// Overloaded constructor that can take an EntityContainer as a parameter so that it can be mocked out by our tests
 /// </summary>
 /// <param name="context">The Entity context</param>
 public Log4NetRepository(MvcLoggingDemoContainer context)
 {
 _context = context;
 }

 /// <summary>
 /// Gets a filtered list of log events
 /// </summary>
 /// <param name="pageIndex">0 based page index</param>
 /// <param name="pageSize">max number of records to return</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevel">The level of the log messages</param>
 /// <returns>A filtered list of log events</returns>
 public IQueryable<LogEvent> GetByDateRangeAndType(int pageIndex, int pageSize, DateTime start, DateTime end, string logLevel)
 {
 IQueryable<LogEvent> list = (from b in _context.Log4Net_Error
 where b.Date >= start && b.Date <= end
 && (b.Level == logLevel || logLevel == "All")
 select new LogEvent { IdType = "number"
 , Id = ""
 , IdAsInteger = b.Id
 , IdAsGuid = Guid.NewGuid()
 , LoggerProviderName = "Log4Net"
 , LogDate = b.Date
 , MachineName = b.Thread
 , Message = b.Message
 , Type = ""
 , Level = b.Level
 , Source = b.Thread
 , StackTrace = "" });

 return list;
 }

 /// <summary>
 /// Returns a single Log event
 /// </summary>
 /// <param name="id">Id of the log event as a string</param>
 /// <returns>A single Log event</returns>
 public LogEvent GetById(string id)
 {
 int logEventId = Convert.ToInt32(id);

 LogEvent logEvent = (from b in _context.Log4Net_Error
 where b.Id == logEventId
 select new LogEvent { IdType = "number"
 , IdAsInteger = b.Id
 , LoggerProviderName = "Log4Net"
 , LogDate = b.Date
 , MachineName = b.Thread
 , Message = b.Message
 , Type = ""
 , Level = b.Level
 , Source = b.Thread
 , StackTrace = ""
 , AllXml = "" })
 .SingleOrDefault();

 return logEvent;
 }

 /// <summary>
 /// Clears log messages between a date range and for specified log levels
 /// </summary>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevels">string array of log levels</param>
 public void ClearLog(DateTime start, DateTime end, string[] logLevels)
 {
 string logLevelList = "";
 foreach (string logLevel in logLevels)
 {
 logLevelList += ",'" + logLevel + "'";
 }
 if (logLevelList.Length > 0)
 {
 logLevelList = logLevelList.Substring(1);
 }

 string commandText = "delete from Log4Net_Error WHERE [Date] >= @p0 and [Date] <= @p1 and Level in (@p2)";

 SqlParameter paramStartDate = new SqlParameter { ParameterName = "p0", Value = start.ToUniversalTime(), DbType = System.Data.DbType.DateTime };
 SqlParameter paramEndDate = new SqlParameter { ParameterName = "p1", Value = end.ToUniversalTime(), DbType = System.Data.DbType.DateTime };
 SqlParameter paramLogLevelList = new SqlParameter { ParameterName = "p2", Value = logLevelList };

 _context.ExecuteStoreCommand(commandText, paramStartDate, paramEndDate, paramLogLevelList);
 }

 }
}
 

11. In the Repository directory, create a new class file and name it, “HealthMonitoringRepository”
12. Replace the contents of the newly created file with the code below :

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

using MvcLoggingDemo.Models.Entities;
using MvcLoggingDemo.Services.Paging;
using MvcLoggingDemo.Helpers;
using System.Data.SqlClient;

namespace MvcLoggingDemo.Models.Repository
{
 /// <summary>
 /// This class extracts information that ASP.NET Health Monitoring stores so that we can report on it
 /// </summary>
 public class HealthMonitoringRepository : ILogReportingRepository
 {
 MvcLoggingDemoContainer _context = null;

 /// <summary>
 /// Default Constructor uses the default Entity Container
 /// </summary>
 public HealthMonitoringRepository()
 {
 _context = new MvcLoggingDemoContainer();
 }

 /// <summary>
 /// Overloaded constructor that can take an EntityContainer as a parameter so that it can be mocked out by our tests
 /// </summary>
 /// <param name="context">The Entity context</param>
 public HealthMonitoringRepository(MvcLoggingDemoContainer context)
 {
 _context = context;
 }

 /// <summary>
 /// Gets a filtered list of log events
 /// </summary>
 /// <param name="pageIndex">0 based page index</param>
 /// <param name="pageSize">max number of records to return</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevel">The level of the log messages</param>
 /// <returns>A filtered list of log events</returns>
 public IQueryable<LogEvent> GetByDateRangeAndType(int pageIndex, int pageSize, DateTime start, DateTime end, string logLevel)
 {
 IQueryable<LogEvent> list = (from h in _context.vw_aspnet_WebEvents_extended
 where h.EventTimeUtc >= start && h.EventTimeUtc <= end
 && (h.Level == logLevel || logLevel == "All")
 select new LogEvent { IdType = "string"
 , Id = h.EventId
 , IdAsInteger = 0
 , IdAsGuid = Guid.NewGuid()
 , LoggerProviderName = "Health Monitoring"
 , LogDate = h.EventTimeUtc
 , MachineName = h.MachineName
 , Message = h.Message
 , Type = h.EventType
 , Level = h.Level
 , Source = h.RequestUrl
 , StackTrace = "" });

 return list;
 }

 /// <summary>
 /// Returns a single Log event
 /// </summary>
 /// <param name="id">Id of the log event as a string</param>
 /// <returns>A single Log event</returns>
 public LogEvent GetById(string id)
 {
 LogEvent logEvent = logEvent = (from b in _context.vw_aspnet_WebEvents_extended
 where b.EventId == id
 select new LogEvent { IdType = "string"
 , Id = b.EventId
 , LoggerProviderName = "Health Monitoring"
 , LogDate = b.EventTimeUtc
 , MachineName = b.MachineName
 , Message = b.Message
 , Type = b.EventType
 , Level = b.Level
 , Source = b.RequestUrl
 , StackTrace = ""
 , AllXml = "" })
 .SingleOrDefault();

 return logEvent;

 }

 /// <summary>
 /// Clears log messages between a date range and for specified log levels
 /// </summary>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevels">string array of log levels</param>
 public void ClearLog(DateTime start, DateTime end, string[] logLevels)
 {
 string logLevelList = "";
 foreach (string logLevel in logLevels)
 {
 logLevelList += ",'" + logLevel + "'";
 }
 if (logLevelList.Length > 0)
 {
 logLevelList = logLevelList.Substring(1);
 }

 string commandText = "";
 commandText += "DELETE ";
 commandText += "FROM ";
 commandText += "    aspnet_WebEvent_Events ";
 commandText += "WHERE";
 commandText += "    EventId IN    ";
 commandText += "(SELECT EventId    ";
 commandText += " FROM vw_aspnet_WebEvents_extended ";
 commandText += " WHERE ";
 commandText += "    [EventTimeUtc] >= @p0";
 commandText += " AND [EventTimeUtc] <= @p1";
 commandText += " AND [Level] IN (@p2)"; // eg:  AND [Level] IN ('Info','Debug')
 commandText += " )";

 SqlParameter paramStartDate = new SqlParameter { ParameterName = "p0", Value = start.ToUniversalTime(), DbType = System.Data.DbType.DateTime };
 SqlParameter paramEndDate = new SqlParameter { ParameterName = "p1", Value = end.ToUniversalTime(), DbType = System.Data.DbType.DateTime };
 SqlParameter paramLogLevelList = new SqlParameter { ParameterName = "p2", Value = logLevelList };

 _context.ExecuteStoreCommand(commandText, paramStartDate, paramEndDate, paramLogLevelList);
 }

 }
}
 

At this point we have 4 stand alone repository classes that can each retrieve information from their own data store (table).

To build our log reporting tool we will need to create a new class that will be able to pull data out of one or all of the repositories that we have just created.

13. In our “Interfaces” folder, create a new file and name it, “ILogReportingFacade.cs”

14. Replace the contents of the newly created file with the code below :

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

using MvcLoggingDemo.Services.Paging;

namespace MvcLoggingDemo.Models.Repository
{
 /// <summary>
 /// This interface provides a facade over all of our LogReport repositories
 /// </summary>
 public interface ILogReportingFacade
 {
 /// <summary>
 /// Gets a filtered list of log events
 /// </summary>
 /// <param name="pageIndex">0 based page index</param>
 /// <param name="pageSize">max number of records to return</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logProviderName">If empty all log providers used, otherwise it will be filtered by the specified log provider</param>
 /// <param name="logLevel">The level of the log messages</param>
 /// <returns>A filtered list of log events</returns>
 IPagedList<LogEvent> GetByDateRangeAndType(int pageIndex, int pageSize, DateTime start, DateTime end, string logProviderName, string logLevel);

 /// <summary>
 /// Returns a single Log event
 /// </summary>
 /// <param name="logProviderName">Name of the log provider</param>
 /// <param name="id">Id of the log event as a string</param>
 /// <returns>A single Log event</returns>
 LogEvent GetById(string logProviderName, string id);

 /// <summary>
 /// Clears log messages for a given date range and log level
 /// </summary>
 /// <param name="logProviderName">Name of the log provider</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevels">The level of the log messages</param>
 void ClearLog(string logProviderName, DateTime start, DateTime end, string[] logLevels);

 /// <summary>
 /// Get's a list of all log providers registered in the web.config file
 /// </summary>
 /// <returns>A list of all log providers registered</returns>
 Dictionary<string, string> GetLogProviders();
 }
}
 

15. In the “Repository” folder, create a new file and name it, “LogReportingFacade.cs”

16. Replace the contents of the newly created file with the code below :

using System;
using System.Configuration;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Web;

using MvcLoggingDemo.Models.Entities;
using MvcLoggingDemo.Services.Logging;
using MvcLoggingDemo.Services.Paging;
using MvcLoggingDemo.Helpers;

namespace MvcLoggingDemo.Models.Repository
{
 /// <summary>
 /// This class provides a facade over all of our LogReport repositories
 /// </summary>
 public class LogReportingFacade : ILogReportingFacade
 {
 MvcLoggingDemoContainer _context = new MvcLoggingDemoContainer();

 private Dictionary<string, string> logProviders = null;

 /// <summary>
 /// Default constructor
 /// </summary>
 public LogReportingFacade()
 {
 Init();
 }

 /// <summary>
 /// Overloaded constructor that can take an EntityContainer as a parameter so that it can be mocked out by our tests
 /// </summary>
 /// <param name="context">The Entity context</param>
 public LogReportingFacade(MvcLoggingDemoContainer context)
 {
 _context = context;

 Init();
 }

 /// <summary>
 ///
 /// </summary>
 private void Init()
 {
 logProviders = new Dictionary<string, string>();

 // Call ConfigurationManager to read the custom logConfiguration
 // of the web.config file and put its contents into an
 // instance of the custom class created for it.
 LogConfigurationSection configSection = ConfigurationManager.GetSection("logConfiguration") as LogConfigurationSection;

 if (configSection == null)
 throw new ApplicationException("Failed to load the Log Configuration section.");
 else
 {
 for (int i = 0; i < configSection.LogProviders.Count; i++)
 {
 logProviders.Add(configSection.LogProviders[i].Name, configSection.LogProviders[i].Type);
 }
 }
 }

 /// <summary>
 /// Creates and returns an instance of a log provider
 /// </summary>
 /// <param name="logProviderName">The type name of the log provider</param>
 /// <returns>An instance of a log provider</returns>
 private ILogReportingRepository GetProvider(string logProviderName)
 {
 string logSourceType = logProviders[logProviderName];

 Type providerType = Type.GetType(logSourceType);

 ILogReportingRepository provider = Activator.CreateInstance(providerType, _context) as ILogReportingRepository;

 return provider;
 }

 /// <summary>
 /// Gets a filtered list of log events
 /// </summary>
 /// <param name="pageIndex">0 based page index</param>
 /// <param name="pageSize">max number of records to return</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logProviderName">name of the log provider</param>
 /// <param name="logLevel">The level of the log messages</param>
 /// <returns>A filtered list of log events</returns>
 public IPagedList<LogEvent> GetByDateRangeAndType(int pageIndex, int pageSize, DateTime start, DateTime end, string logProviderName, string logLevel)
 {
 IQueryable<LogEvent> list = null;

 switch (logProviderName)
 {
 case "All":
 foreach (string providerName in logProviders.Values)
 {
 IQueryable<LogEvent> logList = GetProvider(providerName).GetByDateRangeAndType(pageIndex, pageSize, start, end, logLevel);
 list = (list == null) ? logList : list.Union(logList);
 }
 break;

 default:
 list = GetProvider(logProviderName).GetByDateRangeAndType(pageIndex, pageSize, start, end, logLevel);
 break;
 }

 list = list.OrderByDescending(d => d.LogDate);

 return new PagedList<LogEvent>(list, pageIndex, pageSize);
 }

 /// <summary>
 /// Returns a single Log event
 /// </summary>
 /// <param name="logProviderName">name of the log provider</param>
 /// <param name="id">Id of the log event as a string</param>
 /// <returns>A single Log event</returns>
 public LogEvent GetById(string logProviderName, string id)
 {
 LogEvent logEvent = GetProvider(logProviderName).GetById(id);
 return logEvent;
 }

 /// <summary>
 /// Clears log messages between a date range and for specified log levels
 /// </summary>
 /// <param name="logProviderName">name of the log provider</param>
 /// <param name="start">start date</param>
 /// <param name="end">end date</param>
 /// <param name="logLevels">string array of log levels</param>
 public void ClearLog(string logProviderName, DateTime start, DateTime end, string[] logLevels)
 {
 GetProvider(logProviderName).ClearLog(start, end, logLevels);
 }

 /// <summary>
 /// Returns a list of all registered log providers
 /// </summary>
 /// <returns>A list of all registered log providers</returns>
 public Dictionary<string, string> GetLogProviders()
 {
 return logProviders;
 }

 }
}
 

The class file above makes use of a custom configuration class. The source code for it can be found in the accompanying download at the end of this article.

The interesting part is in the GetByDateRangeAndType method where we determine if we need to return the results from all of our registered log providers or whether we return the results from a single log provider. Each implemented Log Reporting provider returns an IQueryable interface so we can use the power of LINQ to just loop through each one and do a UNION on them.

After we have consolidated all of the results we order them and page the results. All of this happens server side and then the results for each page are returned back to the calling client. Although at this stage we haven’t built our controller and views yet. That will be done in the next article.

Conclusion

In this article we have created the model and the database access layer for our log reporting tool. We have seen how we can query multiple data sources using Linq to Entities UNION keyword.

In the next article we will build our controller and view to display the logs from our various data sources.

Download

The sourcecode for part 1 is on the Downloads tab of the associated Codeplex website

Tagged with: , , , ,
Posted in ASP.NET MVC

Logging in MVC Part 4 – Log4Net

This is part 4 of the MVC Logging series. Other articles in the series are:

Introduction

In this article we will quickly add Log4Net into the website so that later on we can decide whether to use Log4Net or NLog to log our custom messages.

Log4Net is a very widely used logger and it is quite likely that you have 3rd party dependencies in your website that already use Log4Net so it can be a good idea to track any messages that it logs.

Log4Net

The steps we need to follow are:

1. Download Log4Net
2. Add a reference to Log4Net
3. Add a table in our database to store the Log4Net logs
4. Modify the web.config file for Log4Net
5. Implement a Log4NetLogger that implements our ILogger interface.

Setting up the database

Run the following script to create the table that Log4Net will log message to:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[Log4Net_Error](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Date] [datetime] NOT NULL,
	[Thread] [varchar](255) NOT NULL,
	[Level] [varchar](50) NOT NULL,
	[Logger] [varchar](255) NOT NULL,
	[Message] [varchar](4000) NOT NULL,
	[Exception] [varchar](2000) NULL
) ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO

Web.config configuration

Add the following to the top of your web.config file:

<configuration>
  <configSections>
   ...
  <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  ...
  </configSections>
</configuration>

Add the following underneath the configuration element in your web.config file:

<log4net>
    <appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
      <bufferSize value="100" />
      <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      <connectionString value="data source=[Machine]/[Instance];Initial Catalog=[DatabaseName];Integrated Security=True" />
      <commandText value="INSERT INTO Log4Net_Error ([Date],[Thread],[Level],[Logger],[Message],[Exception]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception)" />
      <parameter>
        <parameterName value="@log_date" />
        <dbType value="DateTime" />
        <layout type="log4net.Layout.RawTimeStampLayout" />
      </parameter>
      <parameter>
        <parameterName value="@thread" />
        <dbType value="String" />
        <size value="255" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%thread" />
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@log_level" />
        <dbType value="String" />
        <size value="50" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%level" />
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@logger" />
        <dbType value="String" />
        <size value="255" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%logger" />
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@message" />
        <dbType value="String" />
        <size value="4000" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%message" />
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@exception" />
        <dbType value="String" />
        <size value="2000" />
        <layout type="log4net.Layout.ExceptionLayout" />
      </parameter>
    </appender>

    <!-- Set root logger level to DEBUG and its only appender to A1 -->
    <root>
      <level value="DEBUG" />
      <appender-ref ref="AdoNetAppender" />
    </root>
  </log4net>

Implement a Log4NetLogger

Now let’s create a logger class for Log4Net that implements our ILogger interface (discussed in part 3).1. Create a new folder underneath the Services folder. Our folder structure will be like this:
Services -> Logging -> Log4Net

2. Create a new class file in the new folder and name it ‘Log4NetLogger.cs’.

3. Add the following code to the class:


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

using log4net;

namespace MvcLoggingDemo.Services.Logging.Log4Net
{
 public class Log4NetLogger : ILogger
 {

 private ILog _logger;

 public Log4NetLogger()
 {
 _logger = LogManager.GetLogger(this.GetType());
 }

 public void Info(string message)
 {
 _logger.Info(message);
 }

 public void Warn(string message)
 {
 _logger.Warn(message);
 }

 public void Debug(string message)
 {
 _logger.Debug(message);
 }

 public void Error(string message)
 {
 _logger.Error(message);
 }

 public void Error(Exception x)
 {
 Error(LogUtility.BuildExceptionMessage(x));
 }

 public void Error(string message, Exception x)
 {
 _logger.Error(message, x);
 }

 public void Fatal(string message)
 {
 _logger.Fatal(message);
 }

 public void Fatal(Exception x)
 {
 Fatal(LogUtility.BuildExceptionMessage(x));
 }
 }
}

You will notice that the code above is almost identical to the one we created for NLog. The only difference is the line in the constructor that instantiates the Log4Net logger.

Testing the Log4Net logger

The last step is to write some code that uses our new debugger. Update the Index method of our Activity controller like this:
(You will also need to add a refernce to the namespace, “MvcLoggingDemo.Services.Logging.Log4Net” at the top of your file)

public ActionResult Index()
 {
 IEnumerable list = activityRepository.GetAll();

 NLogLogger logger = new NLogLogger();
 logger.Info("Test message for NLog");

 Log4NetLogger logger2 = new Log4NetLogger();
 logger2.Info("Test message for Log4Net");

 try
 {
 throw new Exception("A test exception");
 }
 catch(Exception ex)
 {
 Console.WriteLine("ERROR - An error has occurred");
 }

 return View(list);
 }

Now go to the Index page for Activities and inspect the Log4Net_Error table to ensure that the message has been logged.

Conclusion

We now have Elmah, NLog, Log4Net and Health monitoring setup and working on our website. The next few articles will focus on building a log reporting viewer so that we can tie all of these things together so that we can see a consolidated and consistent view of everything that is getting logged on our website.

Download

The source code for part 4 is on the Downloads tab of the associate CodePlex website

Tagged with: , , , ,
Posted in ASP.NET MVC

Logging in MVC Part 3 – NLog

This is part 3 of the MVC Logging series. Other articles in the series are:

Introduction

In the previous article we implemented Health Monitoring into our MVC website.

Now we need to provide a way to log our own custom messages so that we can keep track of important events that occur throughout our website.

For example, you may need to log information before and after a financial transaction has occurred on your eCommerce website.

It is easy to write your own logger but as there are already many established logging frameworks already available for .NET I’ve decided to concentrate on 2 of the most popular ones: Log4Net and NLog.

In this article we will concentrate on setting up NLog and in the next article we will take a look at Log4Net.

NLog

Prior to working on this series I had never ever used NLog - I’ve always used Log4Net. However, after spending some time on Rob Connerys blog and looking at his MVC Storefront series and his MVC starter kit I decided to look at NLog so that I have another logging framework in my utility belt. It’s always good to broaden your knowledge and learn a new skill or master a new toolkit.

So what follows is pretty much exactly what Rob Conery did for his MVC starter kit website. However I have extended the default NLog functionality by including 2 new layout renderers.

Here are the steps we need to take to integrate NLog into our website:

1. Download NLog.

2. Create a table in our database to store the NLog messages.

3. Configure our NLog configuration file.

4. Set up a logging interface for our website.

5. Implement an NLog logger that uses our interface to log messages to the database table in step 2.

6. Add some additional layout renders that we will need for NLog.

Let’s tackle each one in order.

Download NLog

1. Go to http://nlog-project.org/ and download NLog.

2. Add a reference to the NLog binary in your website

Create a table in our database to store the NLog messages

NLog is very flexible and it is up to you as to what information you want to store in the database but for our sample website we will be using the script below.

1. Run the following script against your database:

CREATE TABLE [dbo].[NLog_Error](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[time_stamp] [datetime] NOT NULL,
	[host] [nvarchar](max) NOT NULL,
	[type] [nvarchar](50) NOT NULL,
	1 [nvarchar](50) NOT NULL,
	[message] [nvarchar](max) NOT NULL,
	[level] [nvarchar](50) NOT NULL,
	[logger] [nvarchar](50) NOT NULL,
	[stacktrace] [nvarchar](max) NOT NULL,
	[allxml] [ntext] NOT NULL,
 CONSTRAINT [PK_NLogError] PRIMARY KEY CLUSTERED
(
	[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

ALTER TABLE [dbo].[NLog_Error] ADD  CONSTRAINT [DF_NLogError_time_stamp]  DEFAULT (getdate()) FOR [time_stamp]
GO

Configure our NLog configuration file

Create a file called NLog.config in the root directory of your website and paste the following into it:

<?xml version="1.0" ?>
<nlog autoReload="true" throwExceptions="true" internalLogFile="${basedir}/App_Data/nlog.txt" internalLogLevel="Debug"
 internalLogToConsole="true">

 <targets>
 <!--Useful for debugging-->
 <target name="consolelog" type="ColoredConsole"
 layout="${date:format=HH\:mm\:ss}|${level}|${stacktrace}|${message}" />

 <target name="filelog" type="File" fileName="${basedir}/App_Data/Site.log"
 layout="${date}: ${message}" />

 <target name="eventlog" type="EventLog" source="My App" log="Application"
 layout="${date}: ${message} ${stacktrace}" />

 <target name="databaselog" type="Database">

 <dbProvider>sqlserver</dbProvider>

 <!-- database connection parameters -->
 <!-- alternatively you could provide a single 'connectionstring' parameter -->
 <connectionString>Data Source=[Machine]\[Instance];Initial Catalog=[DatabaseName];Integrated Security=SSPI</connectionString>

 <commandText>
 insert into NLog_Error ([time_stamp],[level],[host],[type],1,[logger],[message],[stacktrace],[allxml]) values(@time_stamp,@level,@host,@type,@source,@logger,@message,@stacktrace,@allxml);
 </commandText>

 <parameter name="@time_stamp" layout="${utc_date}" />
 <parameter name="@level" layout="${level}" />
 <parameter name="@host" layout="${machinename}" />
 <parameter name="@type" layout="${exception:format=type}" />
 <parameter name="@source" layout="${callsite:className=true:fileName=false:includeSourcePath=false:methodName=false}" />
 <parameter name="@logger" layout="${logger}" />
 <parameter name="@message" layout="${message}" />
 <parameter name="@stacktrace" layout="${exception:stacktrace}" />
 <parameter name="@allxml" layout="${web_variables}" />

 </target>

 </targets>

 <rules>
 <!--
 <logger name="*" minlevel="Fatal" writeTo="eventlog" />
 -->
 <logger name="*" minlevel="Info" writeTo="filelog" />
 <logger name="*" minlevel="Info" writeTo="databaselog" />
 </rules>

</nlog>

The following link provides a handy reference to all of the layouts used by NLog:
http://nlog-project.org/wiki/Layout_renderers

You may notice that there are a couple of non-standard layouts used in the example above : {utc_date} and {web_variables}. But don’t worry we will come back to them later on in this article.

Set up a logging interface for our website

I’m just going to use the logging interface created by Rob Conery (although I noticed that the Orchard project has a fuller logging interface, but I think the one below is fine for most needs):

Before we add it in, let’s create a folder called ‘Services’ in the root directory of our website and a child folder in there called ‘Logging’.

Now, in the ‘Services -> Logging’ folder add the following code to a new file called, ‘ILogger.cs’ :

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

namespace MySampleApp.Services.Logging
{
 public interface ILogger
 {
 void Info(string message);

 void Warn(string message);

 void Debug(string message);

 void Error(string message);
 void Error(string message, Exception x);
 void Error(Exception x);

 void Fatal(string message);
 void Fatal(Exception x);

 }
}

As per what Rob did, we will also add a helper function that will be used to format an exception. Place this in a file called, ‘LogUtility.cs’:

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

namespace MySampleApp.Services.Logging
{
 public class LogUtility
 {

 public static string BuildExceptionMessage(Exception x)
 {

 Exception logException = x;
 if (x.InnerException != null)
 logException = x.InnerException;

 string strErrorMsg = Environment.NewLine + "Error in Path :" + System.Web.HttpContext.Current.Request.Path;

 // Get the QueryString along with the Virtual Path
 strErrorMsg += Environment.NewLine + "Raw Url :" + System.Web.HttpContext.Current.Request.RawUrl;

 // Get the error message
 strErrorMsg += Environment.NewLine + "Message :" + logException.Message;

 // Source of the message
 strErrorMsg += Environment.NewLine + "Source :" + logException.Source;

 // Stack Trace of the error

 strErrorMsg += Environment.NewLine + "Stack Trace :" + logException.StackTrace;

 // Method where the error occurred
 strErrorMsg += Environment.NewLine + "TargetSite :" + logException.TargetSite;
 return strErrorMsg;
 }
 }
}

Next create a new folder under “Services -> Logging” called “NLog”.

Add the following code to a new file called, “NLogLogger.cs”:

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

using NLog;

namespace MySampleApp.Services.Logging.NLog
{
 public class NLogLogger : ILogger
 {

 private Logger _logger;

 public NLogLogger()
 {
 _logger = LogManager.GetCurrentClassLogger();
 }

 public void Info(string message)
 {
 _logger.Info(message);
 }

 public void Warn(string message)
 {
 _logger.Warn(message);
 }

 public void Debug(string message)
 {
 _logger.Debug(message);
 }

 public void Error(string message)
 {
 _logger.Error(message);
 }

 public void Error(Exception x)
 {
 Error(LogUtility.BuildExceptionMessage(x));
 }

 public void Error(string message, Exception x)
 {
 _logger.ErrorException(message, x);
 }

 public void Fatal(string message)
 {
 _logger.Fatal(message);
 }

 public void Fatal(Exception x)
 {
 Fatal(LogUtility.BuildExceptionMessage(x));
 }
 }
}

By default, NLog can only log the datetime using local time. However, in this series we need to integrate the NLog logs with Elmah and Health Monitoring which are all capable of storing their dates in UTC format. So to do that in NLog we need to create a new layout renderer.

In our NLog folder, add the following code to a new file called, “UtcDateRenderer.cs”:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web;

using NLog;
using NLog.Config;

namespace MySampleApp.Services.Logging.NLog
{
 [LayoutRenderer("utc_date")]
 public class UtcDateRenderer : LayoutRenderer
 {

 ///
 /// Initializes a new instance of the  class.
 ///
 public UtcDateRenderer()
 {
 this.Format = "G";
 this.Culture = CultureInfo.InvariantCulture;
 }

 protected override int GetEstimatedBufferSize(LogEventInfo ev)
 {
 // Dates can be 6, 8, 10 bytes so let's go with 10
 return 10;
 }

 ///
 /// Gets or sets the culture used for rendering.
 ///
 ///
 public CultureInfo Culture { get; set; }

 ///
 /// Gets or sets the date format. Can be any argument accepted by DateTime.ToString(format).
 ///
 ///
 [DefaultParameter]
 public string Format { get; set; }

 ///
 /// Renders the current date and appends it to the specified .
 ///
 /// <param name="builder">The  to append the rendered data to.
 /// <param name="logEvent">Logging event.
 protected override void Append(StringBuilder builder, LogEventInfo logEvent)
 {
 builder.Append(logEvent.TimeStamp.ToUniversalTime().ToString(this.Format, this.Culture));
 }

 }
}

Also, I like how ELMAH stores all of the web request and cookies information so let’s create another custom NLog renderer that will allow us to easily record all of this information.

Add the following code to a new file in the “NLog” folder, called “WebVariablesRenderer.cs”:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web;

using System.Xml;

using NLog;
using NLog.Config;

namespace MySampleApp.Services.Logging.NLog
{
 [LayoutRenderer("web_variables")]
 public class WebVariablesRenderer : LayoutRenderer
 {

 ///
 /// Initializes a new instance of the  class.
 ///
 public WebVariablesRenderer()
 {
 this.Format = "";
 this.Culture = CultureInfo.InvariantCulture;
 }

 protected override int GetEstimatedBufferSize(LogEventInfo ev)
 {
 // This will be XML of an unknown size
 return 10000;
 }

 ///
 /// Gets or sets the culture used for rendering.
 ///
 ///
 public CultureInfo Culture { get; set; }

 ///
 /// Gets or sets the date format. Can be any argument accepted by DateTime.ToString(format).
 ///
 ///
 [DefaultParameter]
 public string Format { get; set; }

 ///
 /// Renders the current date and appends it to the specified .
 ///
 /// <param name="builder">The  to append the rendered data to.
 /// <param name="logEvent">Logging event.
 protected override void Append(StringBuilder builder, LogEventInfo logEvent)
 {
 StringBuilder sb = new StringBuilder();
 XmlWriter writer = XmlWriter.Create(sb);

 writer.WriteStartElement("error");

 // -----------------------------------------
 // Server Variables
 // -----------------------------------------
 writer.WriteStartElement("serverVariables");

 foreach (string key in HttpContext.Current.Request.ServerVariables.AllKeys)
 {
 writer.WriteStartElement("item");
 writer.WriteAttributeString("name", key);

 writer.WriteStartElement("value");
 writer.WriteAttributeString("string", HttpContext.Current.Request.ServerVariables[key].ToString());
 writer.WriteEndElement();

 writer.WriteEndElement();
 }

 writer.WriteEndElement();

 // -----------------------------------------
 // Cookies
 // -----------------------------------------
 writer.WriteStartElement("cookies");

 foreach (string key in HttpContext.Current.Request.Cookies.AllKeys)
 {
 writer.WriteStartElement("item");
 writer.WriteAttributeString("name", key);

 writer.WriteStartElement("value");
 writer.WriteAttributeString("string", HttpContext.Current.Request.Cookies[key].Value.ToString());
 writer.WriteEndElement();

 writer.WriteEndElement();
 }

 writer.WriteEndElement();
 // -----------------------------------------

 writer.WriteEndElement();
 // -----------------------------------------

 writer.Flush();
 writer.Close();

 string xml = sb.ToString();

 builder.Append(xml);
 }

 }
}

The last step is to modify the global.asax.cs file so that NLog is aware of the new layout renderers:

protected void Application_Start()
 {
 AreaRegistration.RegisterAllAreas();

 RegisterRoutes(RouteTable.Routes);

 ControllerBuilder.Current.SetControllerFactory(new ErrorHandlingControllerFactory());

 // Register custom NLog Layout renderers
 LayoutRendererFactory.AddLayoutRenderer("utc_date", typeof(MySampleApp.Services.Logging.NLog.UtcDateRenderer));
 LayoutRendererFactory.AddLayoutRenderer("web_variables", typeof(MySampleApp.Services.Logging.NLog.WebVariablesRenderer));

 }
1</pre>
At this point we are all set up to log our own messages using NLog.

To test NLog out, add some code to one of your controllers like this:

1
public ActionResult Index()
 {
 IEnumerable list = activityRepository.GetAll();

 NLogLogger logger = new NLogLogger();
 logger.Info("We're on the Index page for Activities");

 try
 {
 throw new Exception("A test exception");
 }
 catch(Exception ex)
 {
 logger.Error("An error has occurred", ex);
 }

 return View(list);
 }

Conclusion

To recap, we now have the following in our MVC website:

* Elmah logging unhandled exceptions into the ‘ELMAH_Error’ table in our database.

* ASP.NET Health monitoring logging events to the ‘aspnet_WebEvent_Events’ table in our database.

* NLog logging messages and/or exceptions to the ‘NLog_Error’ table in our database.

I’ve purposefully let each one of these tools log messages to their own tables so that we can swap any of them out without affecting the other ones.

In the upcoming articles we will tie them all together and create a nice looking reporting tool that will consolidate all of the information that we are now logging on our website.

But before we do that in the next article we will quickly add Log4Net support to our website.

Download

The source code for part 3 is on the Downloads tab of the associate CodePlex website

Tagged with: , , , ,
Posted in ASP.NET MVC

Logging in MVC Part 2 – Health Monitoring

This is part 2 of the MVC Logging series. Other articles in the series are:

Introduction

This is the second article in a series. The first article showed how to set up ELMAH to run on an MVC website.

The main focus of ELMAH is to log unhandled exceptions but there are many events that it can’t log for us. That is where ASP.NET Health monitoring comes in!

Health Monitoring

To log such things as when a website starts up, shuts down, recompiles etc the best tool to use is ASP.NET Health Monitoring. Health monitoring is closely tied to the ASP.NET runtime and can log many events that happen on your website:

For those not familiar with what Health Monitoring here is a list of events that can be tracked using it:

* Application starts and stops
* Failed logins and unhandled exceptions
* “Heartbeats”
* Successful and failed login attempts through Membership
* Successful and failed URL and ACL authorizations by authenticated users
* Valid and expired forms authentication tickets
* View state validation failures
* Compilation errors
* Configuration errors
* Unhandled exceptions
* Request validation failures
* Anything that causes request to abort
* Requests queued, processing, or rejected
* Specific or periodic monitoring event
* Process start time and more

One of the best articles to get started with Health Monitoring is the official page here:

http://www.asp.net/hosting/tutorials/logging-error-details-with-asp-net-health-monitoring-cs

Quick setup guide

1. Setup the database we are using so that we can store health monitoring events.

2. Modify the web.config to include a health monitoring section.

Setting up the database

You have 2 choices here. You can choose to store the health monitoring events in a separate database to your normal website or you can choose to store everything in the one database.

When you create a new MVC project in VS2010, a database is already created for you in the app_data folder which should be setup and ready to go. You may have to select “Show all files” to see the database as it is hidden by default.

However, for the sample application we are building in this series I chose to store everything in one database as it is easier to manage and move around one database instead of two.

* Browse to C:\WINDOWS\Microsoft.NET\Framework\<versionNumber>
* Run the following command :
aspnet_regsql.exe -E -S [machinename]\[instancename] -d [databasename] -A all

-E Use Windows authentication
-S server
-d database
-A Select options to install

You should now see all of the aspnet tables in your database:

If you get stuck setting up your database or want to see the full options available see the following article:

http://msdn.microsoft.com/en-us/library/x28wfk74.aspx

Modifying the web.config file

Once the database is configured add the following to your web.config file. The parent tag is <system.web> :


<healthMonitoring enabled="true">
  <eventMappings>
    <clear />
    <!-- Log ALL error events -->
    <add name="All Errors" type="System.Web.Management.WebBaseErrorEvent" startEventCode="0" endEventCode="2147483647" />
    <!-- Log application startup/shutdown events -->
    <add name="Application Events" type="System.Web.Management.WebApplicationLifetimeEvent" startEventCode="0" endEventCode="2147483647" />
  </eventMappings>
  <providers>
    <clear />
    <!-- Provide any customized SqlWebEventProvider information here (such as a different connection string name value -->
    <add connectionStringName="SampleDatabaseConnectionString" maxEventDetailsLength="1073741823" buffer="false" name="SqlWebEventProvider" type="System.Web.Management.SqlWebEventProvider" />
  </providers>
  <rules>
    <clear />
    <add name="All Errors Default" eventName="All Errors" provider="SqlWebEventProvider" profile="Default" minInstances="1" maxLimit="Infinite" minInterval="00:00:00" />
    <add name="Application Events Default" eventName="Application Events" provider="SqlWebEventProvider" profile="Default" minInstances="1" maxLimit="Infinite" minInterval="00:00:00" />
  </rules>
</healthMonitoring>

Don’t forget to change the name of the connection string to the one used by your website.

Viewing the results

ASP.NET Health Monitoring does not come with any way to view the events logged in the database so fire up your website and query the table in your database to ensure that the events are being logged.

Conclusion

Well that’s the end of this article. It was quite short in length but in the upcoming articles I will show how we can display all of these logged events in a much nicer format and throw in some bells and whistles.

Download

The source code for part 2 is on the Downloads tab of the associated Codeplex website

Tagged with: , , , ,
Posted in ASP.NET MVC
Follow

Get every new post delivered to your Inbox.