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
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
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) By now we can test our Wcf service using the tool WcfTestClient.exe, so, let’s compile the solution pressing F6
Considerations
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
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
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 })
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.