sâmbătă, 10 ianuarie 2015

Automatic Updates From SQL Server Using Signalr and Grid.MVC

 

Intro

I was wondering one day if Signalr could be used to automagically update  a web page when a new record is inserted in an SQL Server table. After a bit of digging around, i found that, you guessed it, the answer was YES! So here’s how.
In this post i’ll asume that you know how to create a new MVC 5 project and install a nuget package.

Prerequisites

First, you will need to download and install Visual Studio 2013 Community Edition
Create a new MVC 5 project and then install the following nuget packages: Microsoft.AspNet.SignalR, Grid.MVC and Dapper.NET. We’ll be using Dapper to connect to our database.
Run the following commands in Package Manager Console:

  • Install-Package Microsoft.AspNet.Signalr

  • Install-Package Grid.Mvc

  • Install-Package Dapper

Setting Up the Database

Create a new database named TestDatabase. In this database create a new table named SalesNotifications using the following script:

CREATE TABLE [dbo].[SalesNotifications]
(
    [Id] INT NOT NULL PRIMARY KEY Identity(1,1), 
    [ProductName] NVARCHAR(50) NULL, 
    [Quantity] INT NULL, 
    [SalesNotificationRead] BIT NULL
)

Enable service broker on this database by running the following script:

ALTER DATABASE TestDataBase SET ENABLE_BROKER with rollback immediate
 

Startup.cs and Global.asax

We will use SqlDependency class in order to receive notifications from the server (for this reason the service broker was enabled on the database).

Modify the class Startup.cs in order to enable Signlr. Add the following code in the method Configuration:

app.MapSignalR();

We’ll need to start the listner for receiving change notifications from the server. In order to do that, add the following line in Global.asax in the Application_Start method:

SqlDependency.Start(connString);

When the application ends, we’ll need to stop the listener. Call SqlDependency.Stop in Application_End()

protected void Application_End()
        {
            //Free the dependency
            SqlDependency.Stop(connString);
        }

Hubs

Create a new folder in your solution named Hubs. This will be the place where we’ll put the Signalr hub class. After that, create a new Signalr Hub class in this folder, name it SalesHub. In order to do that, add new item, go to the Web tab and search for Signalr Hub Class (v2):new item signalr hub class 

    [HubName("salesHub")]
    public class SalesHub : Hub
    {
        public void Hello()
        {
            Clients.All.hello();
        }

        public static void UpdateSalesList()
        {
            IHubContext context = GlobalHost.ConnectionManager.GetHubContext<SalesHub>();
            context.Clients.All.updateSalesList();
        }
    }

The method UpdateSalesList() will be called when the OnChangeEventHandler event will be fired and this method will be used to broadcast messages.

Models and Database Access

First, create new model in the Models folder. Name it SalesNotifications.

public class SalesNotifications
    {
        [HiddenInput] 
        public int Id { get; set; }
        public string ProductName { get; set; }
        [Range(0, 1000000000,
            ErrorMessage = "Quantity must be at least zero")]
        public int Quantity { get; set; }
        public bool SalesNotificationRead { get; set; }
    }

For the database access, we’ll use Dapper.NET to access the database. Dapper.NET is a micro-orm, simple and fun to use. It’s made by Marc Gravell and Sam Saffron and it’s a brilliant piece of software.
Create a new folder in your solution named DataAccess. In this folder, add a new class named SalesNotificationsRepository. This class will have 3 methods: GetAllUnreadSalesNotifications() that will pull from the database all unread notifications,
MarkSalesNotificationAsRead(int requestId) will mark a notifications as Read based on Id and dependency_OnChange that will execute every time the SQL Server will send a notification to the web app.

public class SalesNotificationsRepository
    {
        string connectionString = ConfigurationManager.ConnectionStrings["dbConnectionString"].ToString();
        SqlConnection dbcon = new SqlConnection(ConfigurationManager.ConnectionStrings["dbConnectionString"].ToString());
        public List<SalesNotifications> GetAllUnreadSalesNotifications()
        {
           var result = new List<SalesNotifications>();
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();
                using (var command = new SqlCommand(@"select [Id]
                                  ,[ProductName]
                                  ,[Quantity]
                                  ,[SalesNotificationRead]
                                  FROM [dbo].[SalesNotifications] where SalesNotificationRead = 0", connection))
                {
                    command.Notification = null;

                    var dependency = new SqlDependency(command);
                    dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);

                    if (connection.State == ConnectionState.Closed)
                        connection.Open();

                    var reader = command.ExecuteReader();

                    while (reader.Read())
                    {
                        result.Add(item: new SalesNotifications
                        {
                            Id = (int)reader["Id"],
                            ProductName = (string)reader["ProductName"],
                            Quantity = Convert.ToInt32(reader["Quantity"]),
                            SalesNotificationRead = Convert.ToBoolean(reader["SalesNotificationRead"])
                        });
                    }
                }

            }
            return result; 
        }
        public int MarkSalesNotificationAsRead(int requestId)
        {
            var sqlQuery =
                "UPDATE SalesNotifications " +
                "SET SalesNotificationRead = 1 " +
                "WHERE Id = @requestId";
            var result = this.dbcon.Execute(sqlQuery, new { requestId = requestId });
            return result;
        }

        private void dependency_OnChange(object sender, SqlNotificationEventArgs e)
        {
            if (e.Type == SqlNotificationType.Change)
            {
                SalesHub.UpdateSalesList();
            }
        }
    }

The last two methods are pretty simple, so let’s look at the first one. In order to get the data from the SalesNotifications table and to receive the notifications from the SQL Server, we’ll need to associate an instance of SqlDependency class to an SqlCommand

 using (var command = new SqlCommand(@"select [Id]
                                  ,[ProductName]
                                  ,[Quantity]
                                  ,[SalesNotificationRead]
                                  FROM [dbo].[SalesNotifications] where SalesNotificationRead = 0", connection))
                {
                    command.Notification = null;

                    var dependency = new SqlDependency(command);
                    dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);

The select query will pull all the unread sales notifications from the database and dependency_OnChange will be called every time there is a change in the SalesNotifications table data. Note that this select has some limitations.

Controllers and Views

The SalesNotifications controller is very simple. It has only two actions: One will be used to get unread sales notifications and the other to mark a sales notification as read.

 public class SalesNotificationsController : Controller
    {
        SalesNotificationsRepository salesNotificationRepository = new SalesNotificationsRepository();
        // GET: SalesNotifications
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult GetAllUnreadSalesNotifications()
        {
            return PartialView("_SalesNotificationsList", salesNotificationRepository.GetAllUnreadSalesNotifications());
        }

        public ActionResult MarkSalesNotificationAsRead(int Id)
        {
            salesNotificationRepository.MarkSalesNotificationAsRead(Id);
            return View("Index");
        }
    }

In the Views folder, create a new folder named SalesNotifications and an empty view named Index. This view will be used to display the list of unread sales notifications.
Create a new empty partial view in the Shared folder and name it _SalesNotificationsList. Sales notifications will be displayed using Grid.MVC in this partial view.
The Index view will look like this:

@{
    ViewBag.Title = "Sales Notifications";
}

<div class="row">
    <div class="col-md-12">
        <div id="salesNotifications"></div>
    </div>
</div>
@section Scripts{
    <script src="~/Scripts/jquery.signalR-2.1.2.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="/signalr/hubs"></script>
    <script type="text/javascript">
        $(function () {
            // Declare a proxy to reference the hub.
            var salesNotificationsHub = $.connection.salesHub;

            //debugger;
            // Create a function that the hub can call to broadcast messages.
            salesNotificationsHub.client.UpdateSalesList = function () {
                getAllSalesNotifications()

            };
            // Start the connection.
            $.connection.hub.start().done(function () {
                getAllSalesNotifications();
            }).fail(function (e) {
                alert(e);
            });
        });


        function getAllSalesNotifications() {
            var tbl = $('#salesNotifications');
            $.ajax({
                url: '/SalesNotifications/GetAllUnreadSalesNotifications',
                contentType: 'application/html ; charset:utf-8',
                type: 'GET',
                dataType: 'html'
            }).success(function (result) {
                tbl.empty().append(result);
            }).error(function () {

            });
        }
    </script>
}

and the _SalesNotificationsList view will look like this:

@model IEnumerable<MVCAutoUpdateFromSQLServerWithSignalr.Models.SalesNotifications>
@using GridMvc.Html;
@{
    ViewBag.Title = "_SalesNotificationsList";
}

<h2>Sales Notifications</h2>

@Html.Grid(Model).Columns(columns =>
           {

               columns.Add(d => d.Id)
                .Encoded(false)
                .Sanitized(false)
                .SetWidth(130)
                .RenderValueAs(d =>
                    @<b>
                        @Html.ActionLink("Mark As Read", "MarkSalesNotificationAsRead", new { Id = d.Id })
                    </b>);
               columns.Add(foo => foo.ProductName).Sortable(true).Filterable(true).Titled("Product Name");
               columns.Add(foo => foo.Quantity).Sortable(true);
           })

Wraping it up

In this post, Signalr, SqlDependency, ASP.NET MVC 5 where used to  update automatically a web site when new records are inserted in an SQL Server database.
You can download (or fork) a complete solution over at github.

duminică, 14 iulie 2013

On Replacing in Strings in SQL Server - part 2

Part 1 here: http://verrigo.blogspot.ro/2013/06/on-replacing-in-strings-in-sql-server.html

So last time we looked at two strategies for replacing strings using T-SQL. The first one (using repeated calls of replace function), and the second one, using a recursive CTE approach.

Two things to notice. First, with the recursive CTE approach, the replace is configurable and much easier to look at. Second, the performance of the first approach is a lot better than the second one.

Approach Reads Writes Duration
Replace(Replace… 129 0 380
Using CTE 583162 221 1935

 

Let’s CTE again (Is it worth it?)

So, let’s remember the CTE approach again

;WITH s AS 
(SELECT ROW_NUMBER() OVER (ORDER BY ReplaceTest.Name) AS rono, ReplaceTest.Name AS s FROM dbo.ReplaceTest),
replacetble (id, toReplace, replaceWith)
AS
(select 1 as id, 'a' as toReplace, 'b' as replaceWith
Union ALL
select 2 as id,  'b', 'c'
Union ALL
select 3 as id,   'c', 'd'
Union ALL
select 4 as id,  'd', 'e'
Union ALL
select 5 as id,  'e', 'f'),
replacenow(z, idr ) AS
(
SELECT REPLACE(s.s,(select COALESCE(replacetble.toReplace,'')  from replacetble where id =1 ),(select COALESCE(replacetble.replaceWith,'') from replacetble where id =1 ))AS z,1 AS idr FROM s
UNION ALL
SELECT REPLACE(replacenow.z,(SELECT COALESCE(replacetble.toReplace,'') FROM replacetble
WHERE id = idr),(SELECT COALESCE(replacetble.replaceWith,'') FROM replacetble
WHERE id = idr)) AS z, idr+1 FROM replacenow 
WHERE idr>=1 AND idr<=LEN(replacenow.z)
)
SELECT z,idr FROM replacenow 
WHERE idr=(SELECT max(id) FROM replacetble)+1

First thing to do here is to get rid of the replacetable CTE and start using a table. A table and an index on it.

CREATE TABLE [dbo].[ReplaceTable](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [toReplace] [varchar](100) NULL,
    [replaceWith] [varchar](100) NULL,
 CONSTRAINT [PK_ReplaceTable] 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]
GO
SET ANSI_PADDING OFF
GO
GO
insert into ReplaceTable
select 'a' as toReplace, 'b' as replaceWith
Union ALL
select  'b', 'c'
Union ALL
select   'c', 'd'
Union ALL
select   'd', 'e'
Union ALL
select   'e', 'f'
GO

/****** Object:  Index [NonClusteredIndex-20130702-222441]    Script Date: 7/11/2013 10:20:44 PM ******/
CREATE NONCLUSTERED INDEX [NonClusteredIndex-20130702-222441] ON [dbo].[ReplaceTable]
(
    [toReplace] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

/****** Object:  Index [NonClusteredIndex-20130702-225644]    Script Date: 7/11/2013 10:20:44 PM ******/
CREATE NONCLUSTERED INDEX [NonClusteredIndex-20130702-225644] ON [dbo].[ReplaceTable]
(
    [replaceWith] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

Next, we will create a clusterd index on the ReplaceTest table:

/****** Object:  Index [ClusteredIndex-20130702-213612]    Script Date: 7/11/2013 10:30:13 PM ******/
DROP INDEX [ClusteredIndex-20130702-213612] ON [dbo].[ReplaceTest] WITH ( ONLINE = OFF )
GO
 
/****** Object:  Index [ClusteredIndex-20130702-213612]    Script Date: 7/11/2013 10:30:13 PM ******/
CREATE CLUSTERED INDEX [ClusteredIndex-20130702-213612] ON [dbo].[ReplaceTest]
(
    [Name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
 

And now, the code will suffer minor adjustments in order to take advantage of the recent changes:

;WITH replacenow(z, idr ) AS
(
SELECT REPLACE(replacetest.name,(select COALESCE(replacetable.toReplace,'')  from replacetable where id =1 ),(select COALESCE(replacetable.replaceWith,'') from replacetable where id =1 ))AS z,1 AS idr FROM replacetest
UNION ALL
SELECT REPLACE(replacenow.z,(SELECT COALESCE(replacetable.toReplace,'') FROM replacetable
WHERE id = idr),(SELECT COALESCE(replacetable.replaceWith,'') FROM replacetable
WHERE id = idr)) AS z, idr+1 FROM replacenow 
WHERE idr>=1 AND idr<=LEN(replacenow.z)
)
SELECT z,idr FROM replacenow 
WHERE idr=(SELECT max(id) FROM replacetable)+1

Next, we take a look at the two queries again. We will examine the execution plan and use the sql profiler to determine the number of reads, writes and duration for each query.

A plan is a plan is plan…

If so far the conclusion is not obvious by now, check out the photos below showing the execution plan for each query:

replace replace execution plan

Execution plan for the first approach

cte_execution_plan

Execution plan for the CTE approach

Let’s look at the result from SQL Profiler:

profiler_replace_replace

Reads, Writes, Duration for the first approach

profiler_cte

Reads, Writes, Duration for the CTE approach

 

So, to sum it up, there’s really only one way to do this kind of replacing in SQL Server

Approach Reads Writes Duration
Replace(Replace… 144 0 936
Using CTE 823316 341 6961

 

 

Fun with SQL CLR

For the final scenario, we’re gonna take a look at the following request: Strip out all the diacritics in a text and replace them with their corresponding characters. So for example: “astăzi” will become “astazi”.

In order to achieve this and not use so many calls of the replace functions that will make the code look horrible, we will use SQL CLR.

First, let’s create a C# class that will hold our RemoveDiacritics method

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlServer.Server;
using System.Globalization;
 
namespace SQLFunctions
{
    public class SVF
    {
        public static string RemoveDiacritics(string s)
        {
            string stFormD = s.Normalize(NormalizationForm.FormD); 
            StringBuilder sb = new StringBuilder(); 
            for (int ich = 0; ich < stFormD.Length; ich++) { 
                UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(stFormD[ich]); 
                if (uc != UnicodeCategory.NonSpacingMark) { sb.Append(stFormD[ich]); } 
            } 
            return (sb.ToString().Normalize(NormalizationForm.FormC));
        }
    }
}
 
Build the class and then run the following sql code:
sp_configure 'clr enabled', 1
GO
reconfigure
GO
create assembly SQLFunctions from 'SQLFunctions.dll'
GO
create function RemoveDiacritics(@s nvarchar(200)) returns nvarchar(200)
as external name [SQLFunctions].[SQLFunctions.SVF].[RemoveDiacritics]
 
Now, let’s see how a call to our new function compares to the previous methods. First, the execution plan and then the profiler output
SQLCLR Remove Diacritics

SQL CLR RemoveDiacritics execution plan

 

SQLCLR Remove Diacritics profiler

Reads, Writes, Duration for SQL CLR RemoveDiacritics

 

So this method, having 349 reads, 0 writes and 992 ms duration is much nicer to look at and does suffer in terms of performance.

Couple of notes

1. I got the code for the sql clr method that uses the Normalize method from here: http://stackoverflow.com/a/249126/404311

2. I am using SSMS Tools Pack (http://www.ssmstoolspack.com/) to save the execution plans to .jpg files. So big thanks to Mladen Prajdić (@MladenPrajdic) for this

 

Cheers,

sâmbătă, 29 iunie 2013

On Replacing in Strings in SQL Server - part 1

It’s easy: use the replace function in T-SQL

Select replace('aaaaax', 'a', 'b')

But what if we want to replace multiple strings? We have call replace multiple time within itself until it gets so ugly we immediately disown the code and send it to a boarding school in Switzerland just so we don’t see it anymore:

Select replace(replace(replace(replace('abcdefgh', 'a', 'b'), 'b', 'c'), 'c','d'),'d','e')

And if there are 10 characters to replace, it will look like some monsters from deep inside the depths of hell. Or whatever.

Testing

Let’s first create a table containing a sequence of integers, from 1 to 10000. The first time I saw this technique was in Louis Davidson’s book  

Pro SQL Server 2008 Relational Database Design and Implementation (http://www.amazon.com/Server-Relational-Database-Implementation-Experts/dp/143020866X/ref=sr_1_5?ie=UTF8&qid=1372519918&sr=8-5&keywords=louis+davidson+pro+sql+server)

I really like this approach, so here it is:

USE [Tests]
GO
 
/****** Object:  Table [dbo].[sequence]    Script Date: 6/29/2013 8:35:09 AM ******/
SET ANSI_NULLS ON
GO
 
SET QUOTED_IDENTIFIER ON
GO
 
CREATE TABLE [dbo].[sequence](
    [Number] [int] NULL
) ON [PRIMARY]
 
GO
 
 
;with digits as
(
select (1) as number
Union
select (2) as number
Union
select (3) as number
Union
select (4) as number
Union
select (5) as number
Union
select (6) as number
Union
select (7) as number
Union
select (8) as number
Union
select (9) as number
Union
select (0) as number
)
insert into sequence
select d1.number + 10*d2.number+100*d3.number+1000*d4.number +1 from digits d1 cross join digits d2
cross join digits d3
cross join digits d4

Let’s create a test table that will contain just one column (name) and 10000 rows. This table will be used to test the different replace strategies we will consider.

USE [Tests]
GO
 
/****** Object:  Table [dbo].[ReplaceTest]    Script Date: 6/29/2013 11:29:28 PM ******/
SET ANSI_NULLS ON
GO
 
SET QUOTED_IDENTIFIER ON
GO
 
CREATE TABLE [dbo].[ReplaceTest](
    [Name] [nvarchar](100) NULL
) ON [PRIMARY]
 
GO
 
with InitialString as
(Select 'abcdefghijklmnopqrstuvwxyz' as initial)
insert into ReplaceTest
select InitialString.initial+cast(number as varchar(5)) 
from InitialString cross join sequence

So now we have table containing 10000 strings. Notice there is no index on this table, and this is by design. The goal here is to analyze the replace strategies without indexing the table, focusing only on the algorithm.

The Replace Code from Hell

The first replace code to test is the obvious one:

select replace(replace(replace(replace(replace(ReplaceTest.Name,'a', 'b'), 'b', 'c'), 'c', 'd'), 'd', 'e'), 'e', 'f') from ReplaceTest

Using a trace on the sql server, let’s look at the number of reads and query duration for the code above:

ReplaceOfReplace

So we have 129 reads and 380 ms duration.

Replacing using CTE – A Bad Idea?

Let’s do things… a bit differently. Let’s do some replacing using CTE, recursive CTE and create a more configurable, more friendly SQL code.

;WITH s AS 
(SELECT ROW_NUMBER() OVER (ORDER BY ReplaceTest.Name) AS rono, ReplaceTest.Name AS s FROM dbo.ReplaceTest),
replacetble (id, toReplace, replaceWith)
AS
(select 1 as id, 'a' as toReplace, 'b' as replaceWith
Union ALL
select 2 as id,  'b', 'c'
Union ALL
select 3 as id,   'c', 'd'
Union ALL
select 4 as id,  'd', 'e'
Union ALL
select 5 as id,  'e', 'f'),
replacenow(z, idr ) AS
(
SELECT REPLACE(s.s,(select COALESCE(replacetble.toReplace,'')  from replacetble where id =1 ),(select COALESCE(replacetble.replaceWith,'') from replacetble where id =1 ))AS z,1 AS idr FROM s
UNION ALL
SELECT REPLACE(replacenow.z,(SELECT COALESCE(replacetble.toReplace,'') FROM replacetble
WHERE id = idr),(SELECT COALESCE(replacetble.replaceWith,'') FROM replacetble
WHERE id = idr)) AS z, idr+1 FROM replacenow 
WHERE idr>=1 AND idr<=LEN(replacenow.z)
)
SELECT z,idr FROM replacenow 
WHERE idr=(SELECT max(id) FROM replacetble)+1

Two things are important here: The replacetable CTE that simply defines the rules of the replace (first replace ‘a’ with ‘b’, then ‘b’ with ‘c’ and so on) and the replacenow CTE that uses a recursive strategy and actually performs the replace. I think this code looks better than the first one, it is more configurable - simply add a new UNION ALL at the end of the replacetable CTE with the string you’d want to replace:

Union ALL
select 6 as id,  'f', 'g'

But, it has a problem. It is very slow. How slow? Take a look

Replace_CTE

So we have 583162 reads, we have 1935 ms duration and we even have 221 writes, just to show how slow this code really is.

Coming up in part two: A different take on the whole recursive CTE idea and we’ll have fun with some SQL CLR.

Cheers.

duminică, 27 februarie 2011

Unit Testing TSQL in SQL Server 2005and the tSQLT Framework–part 1

 

Installing tSQLT

Installing tSQLT is pretty simple. You can find the .zip file containing two SQL script files here http://sourceforge.net/projects/tsqlt/ . After downloading the zip and extracting it, run the two .sql files on your desired database in this order: SetClrEnabled.sql, tSQLt.class.sql. That’s it, tSQLT has been installed.

There is also a tutorial here: http://www2.sqlity.net/articles/tSQLtTutorial and an user guide here: http://www2.sqlity.net/tsqlt/tsqlt-user-guide 

After running tSQLt.class.sql, you may get the following warnings:

Compiled at 2011-02-27 12:57:27.803
The module 'DefaultResultFormatter' depends on the missing object 'tSQLt.TableToText'. The module will still be created; however, it cannot run successfully until the object exists.
The module 'RunTestClass' depends on the missing object 'tSQLt.Run'. The module will still be created; however, it cannot run successfully until the object exists.
The module 'Run' depends on the missing object 'tSQLt.private_RunTestClass'. The module will still be created; however, it cannot run successfully until the object exists.
The module 'SetFakeViewOff' depends on the missing object 'tSQLt.private_SetFakeViewOff_SingleView'. The module will still be created; however, it cannot run successfully until the object exists.


You can ignore them for now and take a look at your database, and under  stored procedures you should see a whole bunch using the tsql schema:

tsql_storedprocs

tSQLt created those, along with a view, some tables, table-valued functions, scalar-valued functions and other objects, including a CLR assembly.

Testing with tSQLt

Let’s start with a simple test. Testing a scalar-valued function that returns the last working day of previous month.

Here is the function we want to test:

CREATE FUNCTION [dbo].[fnGetLastWorkingDayOfPreviousMonth] 
(
-- Add the parameters for the function here

)
RETURNS datetime
AS
BEGIN
declare @lastdayofpreviousmonth datetime
declare @lastworkingdayofpreviousmonth datetime
if(@@DATEFIRST=7)
begin
select @lastdayofpreviousmonth = DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE()),0))
if(DATEPART(dw, @lastdayofpreviousmonth) between 2 and 6)
set @lastworkingdayofpreviousmonth= @lastdayofpreviousmonth
else
set @lastworkingdayofpreviousmonth = DATEADD(day, -(1+DATEPART(dw, @lastdayofpreviousmonth)%@@DATEFIRST), @lastdayofpreviousmonth)
end
return @lastworkingdayofpreviousmonth

END

First thing to do is set up a test class. This is essentially a schema we assign to all our tests. Using this class, we can run all tests inside it, or choose to run them selectively.

EXEC tSQLt.NewTestClass 'DemoTSQLT';
GO

Creating a test for our function is as easy as creating a stored procedure having the schema 'DemoTSQLT'.  tSQLt provides a nice way to test if two values are equal : tSQLt.assertEquals. The test for our function would look like this:

Create procedure [DemoTSQLT].[test last working day in previous month]
AS
BEGIN
declare @expectedResult datetime
set @expectedResult = cast('01-31-2011' as datetime)
declare @actualResult datetime
select @actualResult = dbo.fnGetLastWorkingDayOfPreviousMonth() 

exec tSQLt.assertEquals @expectedResult, @actualResult
END
GO

Because the function to test was designed to include the time portion of the date, effectively making ‘2011-01-31 23:59:59.000’ the last working day of previous month, this test will fail spectacularly. To run the test and see it fail, we can simply do the following:

tsqlt.Run 'DemoTSQLT'
GO

The output of this test can be seen bellow:

outputasserteqaulsfailed

Here, we ran all tests in  ‘DemoTSQLT ‘ class. To run a specific test, change tsqlt.Run to tsqlt.RunTest like this:

tsqlt.RunTest '[DemoTSQLT].[test last working day in previous month]'
GO

Let’s change the test a bit: Where we cast ‘01-31-2011’  as datetime, let’s change that to ‘2011-01-31 23:59:59.000'. Running the test (tsqlt.Run ‘DemoTSQLT’) now will have this result:

+---------------------+
|Test Execution Sumary|
+---------------------+
 
|No|Test Case Name                                       |Result |
+--+-----------------------------------------------------+-------+
|1 |[DemoTSQLT].[test last working day in previous month]|Success|
-----------------------------------------------------------------------------
Test Case Summary: 1 test case(s) executed, 1 succeeded, 0 failed, 0 errored.
-----------------------------------------------------------------------------

Test succeeded, developer happy, function is ready to be used.

There you have it. There is a lot more to tSQLt to be sure and I will try to cover it as much as possible in the next parts of the series. Have fun and happy testing.

Cheers,
Radu

duminică, 13 februarie 2011

Razor, ASP.NET MVC3 and a quick funny little app

I’ve been trying lately to keep up with Microsoft and the .NET framework but it’s been a loosing battle since the get go. The pace of which Microsoft churns out new features for .NET is amazingly scary if you are not some uber-programmer. So I’ve settled for just some areas, gave up my dreams of learning WPF and Silverlight and now I am focused on ASP.NET MVC (3).

I also have a problem with time management so, to kill  two birds with one stone, here is a little web app that keeps a time log of your actions. The idea is simple: When ever you start doing something that falls into a category or another, simply click the start button for the appropriate category and when you stop… yes, click the stop button. The difference in time (seconds, or minutes, or whatever you choose) is stored in a database, along with the date and time when you completed the action. Here is a little screen:

TimeLogScreenShot

So what do we need?

  1. ASP.NET MVC3
  2. JQuery Timers. I got mine from here: http://plugins.jquery.com/project/timers
  3. (Optional). The excellent CSS3 GitHub-like buttons from http://css3buttons.michaelhenriksen.dk/

Let’s begin. First create a new ASP.NET MVC3 Internet Application and choose Razor as your view engine. Razor is so cool, but totally different from the traditional aspx view engine. You will see why when you play a bit with it.

Create a new database with just two tables in it:

TimeLogDBSchema 

And here is the script for creating the two tables:

USE [TimeLog]
GO
IF EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_Duration]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_Duration]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [DF_LogEntry_Duration]
END


End
GO
IF EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_CreatedOn]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_CreatedOn]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [DF_LogEntry_CreatedOn]
END


End
GO
/****** Object: ForeignKey [FK_LogEntry_Category] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [FK_LogEntry_Category]
GO
/****** Object: Table [dbo].[LogEntry] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[LogEntry]') AND type in (N'U'))
DROP TABLE [dbo].[LogEntry]
GO
/****** Object: Table [dbo].[Category] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Category]') AND type in (N'U'))
DROP TABLE [dbo].[Category]
GO
/****** Object: Table [dbo].[Category] Script Date: 02/13/2011 09:15:48 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Category]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Category](
[CategoryId] [int] IDENTITY(1,1) NOT NULL,
[CategoryName] [varchar](100) NULL,
CONSTRAINT [PK_Category] PRIMARY KEY CLUSTERED
(
[CategoryId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO
SET ANSI_PADDING OFF
GO
/****** Object: Table [dbo].[LogEntry] Script Date: 02/13/2011 09:15:48 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[LogEntry]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[LogEntry](
[LogId] [int] IDENTITY(1,1) NOT NULL,
[CategoryId] [int] NULL,
[Duration] [decimal](8, 2) NULL,
[CreatedOn] [datetime] NULL,
CONSTRAINT [PK_LogEntry] PRIMARY KEY CLUSTERED
(
[LogId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO
/****** Object: Default [DF_LogEntry_Duration] Script Date: 02/13/2011 09:15:48 ******/
IF Not EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_Duration]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_Duration]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] ADD CONSTRAINT [DF_LogEntry_Duration] DEFAULT ((0)) FOR [Duration]
END


End
GO
/****** Object: Default [DF_LogEntry_CreatedOn] Script Date: 02/13/2011 09:15:48 ******/
IF Not EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_CreatedOn]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_CreatedOn]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] ADD CONSTRAINT [DF_LogEntry_CreatedOn] DEFAULT (getdate()) FOR [CreatedOn]
END


End
GO
/****** Object: ForeignKey [FK_LogEntry_Category] Script Date: 02/13/2011 09:15:48 ******/
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] WITH CHECK ADD CONSTRAINT [FK_LogEntry_Category] FOREIGN KEY([CategoryId])
REFERENCES [dbo].[Category] ([CategoryId])
GO
IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] CHECK CONSTRAINT [FK_LogEntry_Category]
GO


Now, go to the Models folder in your MVC app and create a new Entity Framework model based on the database you’ve just created. And that takes care of the back-end.
 
Time for some controller action. There is only one method in your home controller you need to create:
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult InsertTimeLog(int categoryId, int value)
{
LogEntry le = new LogEntry();
Category cat = entities.Categories.First<Category>(c => c.CategoryId == categoryId);
le.Category = cat;
le.Duration = value;
le.CreatedOn = DateTime.Now;
entities.AddToLogEntries(le);
entities.SaveChanges();
return null;
}

You also need to change a bit the Index action to return all the categories in the database. Your home controller should look like this by now:
TimeLogEntities entities = new TimeLogEntities();
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
List<Category> categories = entities.Categories.ToList<Category>();
return View(categories);
}


[AcceptVerbs(HttpVerbs.Post)]
public JsonResult InsertTimeLog(int categoryId, int value)
{
LogEntry le = new LogEntry();
Category cat = entities.Categories.First<Category>(c => c.CategoryId == categoryId);
le.Category = cat;
le.Duration = value;
le.CreatedOn = DateTime.Now;
entities.AddToLogEntries(le);
entities.SaveChanges();
return null;
}

public ActionResult About()
{
return View();
}
}

Delete the default Index View in the Views/Home Folder and create a new one based on the above controller.

Ok, one more step to go: The index view. This is where I learnt to love Razor… The index view should look like this (explanations below the code):

@model IEnumerable<MvcApplication3.Models.Category>

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
<span>Categories</span>
</p>
<div class = "categories">
<ul id="CategoryList">

@foreach (var item in Model) {
<li>
<div class="@item.CategoryName.Replace(" ", "")">
<p>
<span class="Category" >@item.CategoryName</span>
<span style="padding-left:20px"></span>
<a class="big button" id="@String.Concat(@item.CategoryName.Replace(" ", ""),"Start")">Start</a>
<span style="padding-left:20px"></span>
<a style="padding-left:30px;padding-right:30px" class="big negative button" id="@String.Concat(@item.CategoryName.Replace(" ", ""),"Stop")" >Stop</a>
<span id="@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))" style="padding-left:20px">0</span>
</p>
</div>

</li>
}

</ul>
</div>
@foreach (var item in Model)
{
<script type="text/javascript">
   1:  
   2:  var categories = $("div.categories");
   3:      var @String.Concat("active", @item.CategoryName.Replace(" ", "")) = false;
   4:      $('.@item.CategoryName.Replace(" ", "")', categories).find('#@String.Concat(@item.CategoryName.Replace(" ", ""),"Start")').css("cursor", "pointer").click(function (i) {
   5:          if (!@String.Concat("active", @item.CategoryName.Replace(" ", ""))) {
   6:              $("#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))").text(0);
   7:              @String.Concat("active", @item.CategoryName.Replace(" ", "")) = !@String.Concat("active", @item.CategoryName.Replace(" ", ""));
   8:              $(this).parents("div").find('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').everyTime(1000, '@String.Concat("controlled", @item.CategoryName.Replace(" ", ""))', function (i) {
   9:                  $("#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))").text(i);
  10:              });
  11:          }
  12:      }).end().find('#@String.Concat(@item.CategoryName.Replace(" ", ""),"Stop")').css("cursor", "pointer").click(function (i) {
  13:          if (@String.Concat("active", @item.CategoryName.Replace(" ", ""))) {
  14:              @String.Concat("active", @item.CategoryName.Replace(" ", "")) = !@String.Concat("active", @item.CategoryName.Replace(" ", ""));
  15:              $(this).parents("div").find('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').stopTime('@String.Concat("controlled", @item.CategoryName.Replace(" ", ""))');
  16:              $.ajax({
  17:                  url: "/Home/InsertTimeLog/", type: "POST", dataType: "json",
  18:                  data: { categoryId: @item.CategoryId, value: $('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').text() },
  19:                  success: function () {
  20:                    ;;
  21:                  }
  22:              })
  23:          }
  24:      });
  25:   
</script>
}

Basically, what this view does is:
  1. For each category insert a little markup with category name, start/stop buttons and a little something to display the time passed
  2. For each category, generate a javascript to handle the start/stop timer and an ajax call to insert the time spent into the database.

That’s allSmile


Caveats: There are many things missing here: a repository, IoC, it only works for one user, a controller and views for categories and a bit many more, but all I wanted was a simple app and this is about as simple as you can get.