Wednesday 13 June 2012

A quick introduction to ASP.NET MVC3 and WCF-based services architecture

A quick introduction to ASP.NET MVC3 and WCF-based services architecture (30 min)
My intention with this work is to introduce the basics of ASP.NET MVC3 and WCF; the main goal will be to draw a picture of a solution using WCF services in the back-end as well as Entity Framework to handle the Data Access, and ASP.NET MVC3 in the front-end. This work obeys to the need to demonstrate/answer some basic questions to friends (who are proficient in other technologies) about an end to end application using ASP.NET MVC and WCF Services.
To achieve our goal the reader has to follow a series of steps in order to create a simple app to perform the basic CRUD operations for a user’s table.
Minimum Requirements
- Visual C# 2010 Express (minimum)
- SQL server express
A. Creating the first project (UI project).
1) Open Visual Studio
2) Create New Project, Name it MyFirstMVC3App
3) From Installed Templates select Web > ASP.NET MVC3 Web Application
4) Select template: empty and View Engine: Razor
B. Creating a project to host our Business objects
1) Add new project to the solution, right click on the solution icon and Select Add > New Project

 
 
2) From Installed Templates select Class library and Name the project as Domain
 
 
3) Remove Class1.cs
4) Create a folder for this project and name it Entities
5) Right click on the Entities folder and select Add > Class and name it User.cs
6) Replace the code inside User.cs with the code block below containing WCF and MVC attributes; will make sense later when we run the app.
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using System.Data.Objects.DataClasses;
 
 
namespace Domain.Entities
{
    [DataContract]
    public class User
    {
        [DataMember]
        [HiddenInput(DisplayValue = false)]
        [EdmScalarPropertyAttribute(EntityKeyProperty = true, IsNullable = false)]
        public int UserID { get; set; }
 
        [DataMember]
        [Required(ErrorMessage = "Please enter a user name")]
        public string Name { get; set; }
 
        [DataMember]
        [Required(ErrorMessage = "Please enter a description")]
        [DataType(DataType.MultilineText)]
        public string Description { get; set; }
 
        [DataMember]
        [Required(ErrorMessage = "Please enter your email address")]
        [RegularExpression(".+\\@.+\\..+", ErrorMessage = "please enter a valid email")]
        public string Email { get; set; }
 
        [DataMember]
        [Required(ErrorMessage = "Please enter your phone number")]
        public string Phone { get; set; } 
 
               public string PrivateField { get; set; }
    }
}
7) You might notice that some attributes are red-highlighted, to correct the problem add the following references to your project, right click at the References node of the current project created and then select Add reference
Copy & paste one by one the following references in the text box at the bottom of the window just opened and then click add (Don’t copy the dot at the beginning of the path !!).
  • c:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 3\Assemblies\System.Web.Mvc.dll
  • C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Runtime.Serialization.dll
  • C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Data.Entity.dll
  • C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.ComponentModel.DataAnnotations.dll

Close the window after adding the last reference.
 

C. Creating a project to host the Data access layer (Entity framework)
0) Create a database, execute the following script in sql server management studio.
(Make sure that you have a folder C:\Temp as shown in the script)

CREATE DATABASE [DBTest] ON PRIMARY
( NAME = N'DBTest', FILENAME = N'C:\Temp\DBTest.mdf' , SIZE = 3072KB, MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
LOG ON
( NAME = N'DBTest_log', FILENAME = N'C:\Temp\DBTest_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
GO
USE DBTest
GO
CREATE TABLE [dbo].[Users]( [UserID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](150) NOT NULL,
[Description] [nvarchar](100) NULL,
[Email] [nvarchar](100) NULL,
[Phone] [nvarchar](100) NULL,
[PrivateField] [nvarchar](100) NULL,
CONSTRAINT [PK_users] PRIMARY KEY NONCLUSTERED
(
[UserID] 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
1) Add a new project to the solution, right click on the solution icon and Select Add > New Project
2) From Installed Templates select Class library and Name the project as Domain.EF
 

3) Remove Class1.cs
4) Right click on Domain.EF (new project) select Add >New Item… from Installed Templates select Data > ADO.NET Entity Data Model and name it EFModel and then click Add
 
5) Select Generate from Database
6) Select your Database connection (You will have to configure a new connection, Click in New Connection, Select MS SQL Server…Server Name: localhost most of the time would be enough, Database name:DBTest) and then Next


7) Select Tables/Finish
8) Right Click on the model you just created, select Properties and change Code Generation Strategy to None (Save and close the file)
9) Right click on the project (Domian.EF), select Add > Class and name the class EFContext.cs
10) Replace the next code in the EFContext file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects;
 
namespace Domain.Entities
{
    public class EFContext : ObjectContext
    {
        public EFContext() : base("name=DBTestEntities", "DBTestEntities")
        {
            _users = CreateObjectSet<User>("Users");
        }
 
        public ObjectSet<User> Users
        {
            get
            {
                if (_users == null)
                {
                    _users = base.CreateObjectSet<User>("Users");
                }
                return _users;
            }
        }
        private ObjectSet<User> _users;
 
    }
}
11) You will notice that VS will prompt that User type or namespace could not be found, right click on references then select Add references > Projects, double click on Domain and then close the window
12) Right click on any of the errors prompted by VS and select Resolve > using Domain.Entities;



D. Creating a WCF-based services architecture project
1) Add a new project to the solution
2) Select Installed Templates > WCF > WCF Service Application and name it WcfService
3) Now add Domain and Domain.EF references to this project (Add references > Projects, double click on Domain and double click on Domain.EF and close the window)
4) Open the file IService1.cs, remove the content inside interface IService1 (what is inside the brakets { }), also remove completely the class CompositeType and insert the following code inside the interface IService1
 
         [OperationContract]
         List<User> GetUsers(int page);
5) Again VS, prompts an error in User type, right click and Resolve.
6) Open Service1.svc, remove the code inside the class Service1 and insert the following code:
 
 
public List<User> GetUsers(int page)
{
  int PageSize = 5;
  EFContext context = new EFContext();
  return context.Users.AsQueryable().OrderBy(u => u.UserID).Skip((page - 1) * PageSize).Take(PageSize).ToList();
}
 
 
 
7) Right click on User type (List<User>) and resolve
8) Add a new reference, this time from Assemblies select System.Data.Entity (Don’t forget to resolve EFContext missing reference)
9) Open the file app.config from Domain.EF and copy the ConnectionString section into Web.Config from WcfService




10) By now we can test our Wcf service using the tool WcfTestClient.exe, so, let’s compile the solution pressing F6
Testing the WCF service.
11) Right clicking on WcfService project, select Debug > Start new instance.
12) In the System Tray you will see ASP.NET Development server is running our WCF service, double click on the icon, a window will popup, click in the Root URL link, a browser will list the content of the project, click on Service1.svc file and copy the link exposed there (that should be similar to http://localhost:50178/Service1.svc?wsdl).

13) Now we need to find and run Wcftestclient to test our service (should be found in the folder "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\WcfTestClient.exe").
14) Once WcfTestClient is open, select file>Add Service, paste the end point address (http://localhost:50178/Service1.svc?wsdl) and click Ok, now you can test your service by double clicking GetUsers(), setting the request value parameter to one (1) (a value of zero will crash !!!) and click invoke.
(Go to SQL Server Management studio to insert a record in your table to see your service returning something other than an empty Domian.Entities.User[])



E. Working the front-end
Let’s go back to the main project (MyFirstMVC3App), we will create the structure to present a web page with a list of users and two buttons to add and update users.
1) Start by adding a new reference to this project, right click on References >Add Reference>Projects>Solution Double click on Domain project and close
(don’t forget to stop debugging the service )
2) Lets wire up our first WCF service, by adding a service reference to our project, right click on References > Add Service Reference, copy again the address of your web service (something similar to http://localhost:50178/Service1.svc?wsdl *port may be different) click Go and make sure that your Namespace is: ServiceReference1 and click OK
3) Now Add our first controller, Right click on the folder “Controllers”, Select Add > Controller and change the name of the controller from Default1Controller to UserController, click Add and replace the code inside the class User Controller with the following:
public ViewResult List(int page = 1)
{
  ServiceReference1.Service1Client myService = new ServiceReference1.Service1Client();
  return View(myService.GetUsers(page));
}
 
4) Add a Model to provide the list of users to our view (will create the view in the next step), Right click in Models > Add > Class name it UsersViewModel.cs and click Add.
Our View Model should look like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Domain.Entities;
 
namespace MyFirstMVC3App.Models
{
    public class UsersViewModel
    {
        public List<User> Users { get; set; }
    }
}
(Don’t forget to resolve before building the solution)
Press F6 to build solution!!
5) Switch to UserController.cs, Right click within the method List and select Add View, select create a strongly typed view Model class UsersViewModel (MyFirstMVC3App.Models)

6) Replace the content inside the body tags for the view you’ve just generated with the following content
    <div>
        <table width="90%" align="center">
            <tableheader>
                <tr>
                    <th align="left">Name</th>
                    <th align="left">Description</th>
                    <th align="right">Phone</th>
                    <th align="right">Email</th>
                </tr>
            </tableheader>
            <tablebody>
            @foreach (var u in Model.Users)  
            {
                <tr>
                    <td align="left">@u.Name</td>
                    <td align="left">@u.Description</td>
                    <td align="right">@u.Phone</td>
                    <td align="right">@u.Email</td>
                </tr>
            }
            </tablebody>
        </table>
 
    </div>
 
7) Now that we have a model (UsersViewModel), lets update our UserController method named List which should look like the following :
        (Don’t forget to resolve UsersViewModel)
 
public ViewResult List(int page = 1)
{
ServiceReference1.Service1Client myService = new ServiceReference1.Service1Client();
UsersViewModel usersViewModel = new UsersViewModel();
usersViewModel.Users = myService.GetUsers(page).ToList();
return View(usersViewModel);
}
8) Open the file global.asax and replace RegisterRoutes method with the following code:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
routes.MapRoute(
"Default", "{controller}/{action}/{id}", new { controller = "User", action = "List", id = UrlParameter.Optional } );
 
}
 
 
 
9) If you have a user in your database, press F5 to see how it looks.
--Here a quick insert to have something to view.
INSERT INTO [DBTest].[dbo].Users(Name, Description, Email, Phone, PrivateField)
VALUES('Dwyane Wade', 'American basketball player', 'test@email.com', '333 333 3333', 'private note')
F. Adding the last pieces of the work
1) Stop the project, and add a the following method to UserController
/// <summary>
/// GET: /User/Add
/// </summary>
/// <returns></returns>
public ActionResult Add()
{
User userModel = new User();
return View("Detail", userModel);
}
2) Right click on the previous method added and select Add View, select “Create a strongly-typed view, Model: User, Scaffold template: Create and click Add button.


3) The following code shows the completed code generated by the wizard except for the code that has been removed related to the “PrivateField”,
also I have added some parameters to Html.BeginForm to trigger the Save method from UserController in the form of a POST request and @Html.HiddenFor
@model Domain.Entities.User
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Add</title>
</head>
<body>
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm("Save", "User", FormMethod.Post))
{
@Html.HiddenFor(model => model.UserID)
@Html.ValidationSummary(true)
<fieldset>
<legend>User</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Description)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Email)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Email)
@Html.ValidationMessageFor(model => model.Email)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Phone)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Phone)
@Html.ValidationMessageFor(model => model.Phone)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "List")
</div>
</body>
</html>
4) We need the user to be able to request the previous view (Detail.cshtml) throughout our List.cshtml view, so let’s open List.cshtml view and add the following code before the ending tag </body>
@using (Html.BeginForm("Add", "User", FormMethod.Get))
{
<input type="submit" value='Add' />
}
Adding a new service:
5) We will now add new service to our interface at the back end layer, Open IService1 and insert the following code beneath GetUsers
         [OperationContract]
bool SaveUser(User user);
6) Open Service1.svc and insert the following code below GetUsers
/// <summary>
/// Stores/Update user data into table dbo.Users
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public bool SaveUser(User user)
{
try
{
EFContext context = new EFContext();
if (user.UserID == 0)
{
context.Users.AddObject(user);
}
else
{
context.Users.Attach(new User() { UserID = user.UserID });
context.Users.ApplyCurrentValues(user);
// Update a existing user
}
context.SaveChanges();
}
catch
{
return false; //something wrong happened
}
return true; //save successfully
}
7) The previous changes to our Service1 requires to update the service reference…from MyFirstMVC3App -> Service references, right click at ServiceReference1 and select Update Service Reference


 
8) Open UserControler.cs and add the following method to the class UserController, this method will take action when the user clicks the Create button from our view Detail.cshtml
[HttpPost]
public ActionResult Save(User userModel)
{
ServiceReference1.Service1Client myService = new ServiceReference1.Service1Client();
var response = myService.SaveUser(userModel);
if (!response)
{
//do something (show a user friendly message!)
}
ActionResult redirectResult = RedirectToAction("List");
return redirectResult;
}
9) Run your app and add a user, your Add view should look like the follow one (try entering an invalid email account):



Adding the necessary functionality to Update a user profile.
10) Open IService1.cs and add the following code to the interface IService1 (beneath SaveUser)
[OperationContract]
User Get(int userId);
11) Open Service1.svc.cs and add the implementation of the method Get to Service1 class (beneath SaveUser)
public User Get(int userId)
{
EFContext context = new EFContext();
var user = context.Users.Where(u => u.UserID == userId).FirstOrDefault();
return user;
}
12) Update service references from MyFirstMVC3App -> Service references, right click at ServiceReference1 and select Update Service Reference
13) Open UserControler.cs and add the following method to the class UserController, this method will take action when the user clicks on an existing user name
public ActionResult Get(int userId)
{
ServiceReference1.Service1Client myService = new ServiceReference1.Service1Client();
var userModel = myService.Get(userId);
return View("Detail", userModel);
}
14) Open List.cshtml and replace @u.Name ( located at the foreach loop )with the following code:
@Html.RouteLink(u.Name, new { controller = "User", action = "Get", userId = u.UserID })
The full block looks like the following:


15) Open Detail.cshtml and replace <input type="submit" value="Create" /> with the following code:
if (Model.UserID == 0)
{
  <input type="submit" value="Create" />
}
else
{
  <input type="submit" value="Update" />
}
16) Run the app and play with it… the missing operation is to remove a user and I thought it would be good to leave it up to the reader as an exercise to give it shot.

 Considerations
· Code refactoring (i.e. a thin layer to implement the services).
· Adopt Test driven development and Inversion of Control (IoC).
· Use proper comments and validations.
· At the Back End
Code refactoring is required
Web services transport protocols: HTTP (or HTTPS), TCP, Named Pipes, and MSMQ.
· At the Front End
Model binders.
Css
JQuery
Javascript.