Monday, November 11, 2013

Display MongoDB Data Using ASP.NET MVC

In last post I presented a logging server solution for the mobile platform using node.js and MongoDB. Today I will build a ASP.NET MVC application to display the logged data stored in MongoDB.

First download MongoDB latest driver from github. There're a few download options out there. I just used the msi installer, and two key dlls, MongoDB.Driver.dll and MongoDB.Bson.dll, were installed in my C:\Program Files (x86)\MongoDB\CSharpDriver 1.8.3\ folder. Then we are ready to create a ASP.NET MVC 3 or MVC 4 project using basic template in Visual Studio 2010, and add MongoDB.driver.dll and MongoDB.Bson.dll to the project References.

The mongoDB database can be running inside the same machine or in a separate machine. The only difference is the connection string, which follows the format of "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]". In my example here I have one MongoDB instance running locally so my connection string is "mongodb://localhost:27017".

Next we create a log item model and a repository to interact with MongoDB with three actions available: search logs within a time span, get original log detail by its ID, and delete a log by its ID (for simplicity reason exception handling is skipped):

using System;
using System.Collections.Generic;
using System.Linq;
 
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Builders;
 
namespace LogsReporting
{
    public class LogItem
    {
        public ObjectId ID { get; set; }
        public DateTime Date { get; set; }
        public string OS { get; set; }
        public string Version { get; set; }
        public string Level { get; set; }
        public string Manufacturer { get; set; }
        public string Model { get; set; }
        public string ScreenSize { get; set; }
        public string Language { get; set; }
        public string Orientation { get; set; }
        public string Timezone { get; set; }
        public string Message { get; set; }
    }
 
    public interface ILogsRepository 
    {
        string GetOriginalLog(string id);
        void DeleteLog(string id);
        IEnumerable<LogItem> GetLogItems(DateTime from, DateTime to);
    }
 
    public class LogsRepository : ILogsRepository
    {
        private MongoCollection<BsonDocument> _collection; 
        public LogsRepository()
        {
            string connectionString = "mongodb://localhost:27017";
            MongoServer server = new MongoClient(connectionString).GetServer();
            MongoDatabase db = server.GetDatabase("Data");
            _collection = db.GetCollection("Logs");
        }
 
        public string GetOriginalLog(string id)
        {
            ObjectId bsonId;
            if (ObjectId.TryParse(id, out bsonId))
            {
                var item = _collection.FindOneById(bsonId);
                if (item != null)
                    return item.ToString();
            }
            return string.Empty;
        }
 
        public void DeleteLog(string id)
        {
            ObjectId bsonId;
            if (ObjectId.TryParse(id, out bsonId))
            {
                _collection.Remove(Query.EQ("_id", bsonId));
            }
        }
 
        public IEnumerable<LogItem> GetLogItems(DateTime from, DateTime to)
        {
            List<LogItem> logs = new List<LogItem>();
            var docs = _collection.Find(Query.And(Query.GTE("time", from), Query.LTE("time", to)))
                .SetSortOrder(SortBy.Descending("time"));
            foreach (var doc in docs)
            {
                LogItem log = new LogItem()
                {
                    ID = doc.GetValue("_id").AsObjectId,
                    Date = TimeZone.CurrentTimeZone.ToLocalTime(doc.GetValue("time").ToUniversalTime()),
                    OS = doc.GetValue("os", "").ToString(),
                    Version = doc.GetValue("version", "").ToString(),
                    Manufacturer = doc.GetValue("manufacturer", "").ToString(),
                    Model = doc.GetValue("model", "").ToString(),
                    Language = doc.GetValue("lang", "").ToString(),
                    ScreenSize = doc.GetValue("screen", "").ToString(),
                    Orientation = doc.GetValue("orientation", "").ToString(),
                    Timezone = doc.GetValue("timezone", "").ToString(),
                    Level = doc.GetValue("level", "").ToString(),
                    Message = doc.GetValue("log", "").ToString()
                };
                if (!string.IsNullOrEmpty(log.Message) && log.Message.Length > 40)
                    log.Message = log.Message.Substring(0, 40) + " ...";
 
                logs.Add(log);
            }
            return logs;
        }
    }
}
MongoDB is schemaless, and can store arbitrary format of data. For convenience let's assume the logged data has following fields: time, os, version, manufacturer, model, language, screen-size, orientation, time-zone, log-level, and log message (refer to Windows 8 logging & reporting). Only "time" field (time stamp) is mandatory here so we could safely filter log entries by time.

The controller provides three actions: show recent logs, show the detail of a given log and delete a log. For demo purpose we only show data logged in the past week:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace LogsReporting
{
    public class LogsController : Controller
    {
        private static readonly ILogsRepository _logs = new LogsRepository();
 
        public ActionResult Index()
        {
            var logs = _logs.GetLogItems(DateTime.Now.AddDays(-7), DateTime.Now);
            return View(logs);
        }
 
        public string Detail(string id)
        {
            var originalLog = _logs.GetOriginalLog(id);
            return originalLog;
        }
 
        public ActionResult Delete(string id)
        {
            _logs.DeleteLog(id);
            return RedirectToAction("Index");
        }
    }
}
The presentation layer is pretty straightforward: a grid view shows the logs' info with "Detail" and "Delete" links. Clicking "Detail" link will open up a new window to display the raw log message. A confirmation popup prompts for a "Delete" action:
@model IEnumerable<LogsReporting.LogItem>
 
<script type="text/javascript">
    function showDetailPopup(id) {
        var url = '@Url.Action("Detail")' + '/' + id;
        window.open(url, "detailWindow", 'width=600px,height=400px');
    }
</script> 
 
<h2 style="text-align: center;">Logs Reporting</h2>
 
<table cellpadding="5px">
    <tr>
        <th>@Html.DisplayNameFor(model => model.Date)</th>
        <th>@Html.DisplayNameFor(model => model.OS)</th>
        <th>@Html.DisplayNameFor(model => model.Version)</th>
        <th>@Html.DisplayNameFor(model => model.Manufacturer)</th>
        <th>@Html.DisplayNameFor(model => model.Model)</th>
        <th>@Html.DisplayNameFor(model => model.ScreenSize)</th>
        <th>@Html.DisplayNameFor(model => model.Language)</th>
        <th>@Html.DisplayNameFor(model => model.Orientation)</th>
        <th>@Html.DisplayNameFor(model => model.Timezone)</th>
        <th>@Html.DisplayNameFor(model => model.Level)</th>
        <th>@Html.DisplayNameFor(model => model.Message)</th>
        <th></th>
    </tr>
@{int i = 0;}
@foreach (var item in Model) {
    <tr style='font-size: 9pt; @(i++%2==0 ? "background-color: #bbbbbb" : "")'> 
        <td>@Html.DisplayFor(modelItem => item.Date)</td>
        <td>@Html.DisplayFor(modelItem => item.OS)</td>
        <td>@Html.DisplayFor(modelItem => item.Version)</td>
        <td>@Html.DisplayFor(modelItem => item.Manufacturer)</td>
        <td>@Html.DisplayFor(modelItem => item.Model)</td>
        <td>@Html.DisplayFor(modelItem => item.ScreenSize)</td>
        <td>@Html.DisplayFor(modelItem => item.Language)</td>
        <td>@Html.DisplayFor(modelItem => item.Orientation)</td>
        <td>@Html.DisplayFor(modelItem => item.Timezone)</td>
        <td>@Html.DisplayFor(modelItem => item.Level)</td>
        <td>@Html.DisplayFor(modelItem => item.Message)</td>
        <td>
            <a href="#" onclick="showDetailPopup('@item.ID');">Details</a> |
            @Html.ActionLink("Delete", "Delete", new { id = item.ID }, 
                new { onclick = "return confirm('Are you sure to delete this log entry?');" })
        </td>
    </tr>
}
</table>

Following screen-shot shows how the view looks like:

Updated on Nov. 12 Just for fun I created a traditional ASP.NET web form .aspx page to show the exact same view:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="LogReporting.aspx.cs" Inherits="LogsReporting.LogReporting" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Logs Reporting</title>
    <style type="text/css">
        h2 {text-align: center;}
        table {text-align: left; padding: 5px;}
        table td, .detail {font-size: 9pt; padding: 5px;}
    </style>
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            $("tr:odd").css("background-color", "#bbbbbb");
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Panel ID="gridPanel" runat="server">
            <h2>Logs Reporting</h2>
            <asp:GridView ID="gvLogs" runat="server" AutoGenerateColumns="False" GridLines="None"
                OnRowDataBound="gvLogs_RowDataBound" EmptyDataText="No logs available">
                <Columns>
                    <asp:BoundField DataField="Date" HeaderText="Date"></asp:BoundField>
                    <asp:BoundField DataField="OS" HeaderText="OS"></asp:BoundField>
                    <asp:BoundField DataField="Version" HeaderText="Version"></asp:BoundField>
                    <asp:BoundField DataField="Language" HeaderText="Language"></asp:BoundField>
                    <asp:BoundField DataField="Manufacturer" HeaderText="Manufacturer"></asp:BoundField>
                    <asp:BoundField DataField="Model" HeaderText="OS"></asp:BoundField>
                    <asp:BoundField DataField="ScreenSize" HeaderText="Version"></asp:BoundField>
                    <asp:BoundField DataField="Level" HeaderText="Level"></asp:BoundField>
                    <asp:BoundField DataField="Message" HeaderText="Message"></asp:BoundField>
                    <asp:TemplateField>
                        <ItemTemplate>
                            <asp:LinkButton ID="lbtDetail" runat="server" Text="Detail"></asp:LinkButton>
                            <asp:LinkButton ID="lbtDelete" runat="server" Text="Delete" OnClick="lbtDelete_Click"
                                OnClientClick="return confirm('Are you sure to delete this log entry?');"></asp:LinkButton>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
        </asp:Panel>
 
        <asp:Panel ID="detailPanel" runat="server" CssClass="detail">
            <asp:Literal ID="logDetail" runat="server"></asp:Literal>
        </asp:Panel>
    </div>
    </form>
</body>
</html>
The code behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Configuration;
 
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Builders;
 
namespace LogsReporting
{
    public partial class LogReporting : System.Web.UI.Page
    {
        private ILogsRepository _repo = new LogsRepository();
 
        protected void Page_Load(object sender, EventArgs e)
        {
            bool showDetail = !string.IsNullOrEmpty(Request.QueryString["id"]);
            detailPanel.Visible = showDetail;
            gridPanel.Visible = !showDetail;
 
            if (showDetail)
            {
                logDetail.Text = _repo.GetOriginalLog(Request.QueryString["id"]);
            } 
            else if (!IsPostBack)
            {
                var dataSource = _repo.GetLogItems(DateTime.Today.AddDays(-7), DateTime.Now);
                bindDataSource(dataSource);
            } 
        }
 
        private void bindDataSource(IEnumerable<LogItem> logs)
        {
            gvLogs.DataSource = logs;
            gvLogs.DataBind();
        }
 
        protected void gvLogs_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (e.Row.RowType == DataControlRowType.DataRow)
            {
                LogItem item = e.Row.DataItem as LogItem;
                LinkButton lbtDetail = e.Row.FindControl("lbtDetail") as LinkButton;
                LinkButton lbtDelete = e.Row.FindControl("lbtDelete") as LinkButton;
                string detailUrl = string.Format("{0}?id={1}", Request.Url.AbsolutePath, item.ID);
                lbtDetail.Attributes.Add("onClick", "window.open('" + detailUrl + "', '', 'width=600,height=400')");
                lbtDelete.CommandArgument = item.ID.ToString();
              }
        }
 
        protected void lbtDelete_Click(object sender, EventArgs e)
        {
            var logID = Convert.ToString(((LinkButton)sender).CommandArgument);
            _repo.DeleteLog(logID);
            var dataSource = _repo.GetLogItems(DateTime.Today.AddDays(-7), DateTime.Now);
            bindDataSource(dataSource);
        }
    }
}