Anda di halaman 1dari 129

Building Single Page Applications using Web API and

angularJS (Free e-book)


BY CHRISTOS S. ON AUGUST 23, 2015 • ( 157 )
Single Page Applications are getting more and more attractive nowadays for two basic reasons.
Website users have always preferred a fluid user experience than one with page reloads and the
incredible growth of several JavaScript frameworks such as angularJS. This growth in
conjunction with all the powerful server side frameworks makes Single Page
Application development a piece of cake. This post is the online version of the free e-book and
describes step by step how to build a production-level SPA using ASP.NET Web API
2 and angularJS . You have two choices. Either grab a coffee and keep going on with this
version or simply download the e-book and enjoy it whenever and wherever you want.

There are a lot of stuff to build in this application so I will break this post in the following
sections:
 What we are going to build: Describe the purpose and the requirements of our SPA
 What we are going to use: The list of technologies and all server and front-end side
libraries and frameworks
 SPA architecture: The entire design of our SPA application from the lowest to the highest
level
 Domain Entities and Data repositories: Build the required Domain Entities and Data
repositories using the generic repository pattern
 Membership: Create a custom authentication mechanism which will be used for Basic
Authentication through Web API
 Single Page Application: Start building the core SPA components step by step
 Discussion: We ‘ll discuss the choices we made in the development process and
everything you need to know to scale up the SPA

Ready to go? Let’s start!

What we are going to build


We are going to build a Singe Page Application to support the requirements of a Video
Rental store that is a store that customers visit, pick and rent DVDs. Days later they come back
and return what they have borrowed. This Web application is supposed to be used only by the
rental store’s employees and that’s a requirement that will affect mostly the front-end
application’s architecture. Let’s see the requirements along with their respective screenshots:

Requirement 1: Home page


1. Latest DVD movies released added to the system must be displayed
2. For each DVD, relevant information and functionality must be available, such as
display availability, watch YouTube trailer and its rating
3. On the right side of the page, genre statistics are being displayed

4. This page should be accessible to unauthenticated users

Requirement 2: Customers
1. There will be 2 pages related to customers. One to view and edit them and another for
registration

2. Both of the pages must be accessible only to authenticated users

3. The page where all customers are being displayed should use pagination for faster results.
A search textbox must be able to filter the already displayed customers and start a new
server side search as well

4. Customer information should be editable in the same view through a modal popup
window
Requirement 3: Movies
1. All movies must be displayed with their relevant information (availability, trailer etc..)
2. Pagination must be used for faster results, and user can either filter the already displayed
movies or search for new ones

3. Clicking on a DVD image must show the movie’s Details view where user can
either edit the movie or rent it to a specific customer if available. This view is accessible
only to authenticated users
4. When employee decides to rent a specific DVD to a customer through the Rent view, it
should be able to search customers through an auto-complete textbox

5. The details view displays inside a panel, rental-history information for this movie, that is
the dates rentals and returnings occurred. From this panel user can search a specific rental
and mark it as returned
6. Authenticated employees should be able to add a new entry to the system. They should be
able to upload a relevant image for the movie as well
Requirement 4: Movie Rental History
1. There should be a specific view for authenticated users where rental history is being
displayed for all system’s movies. History is based on total rentals per date and it’s being
displayed through a line chart

Requirement 5: Accounts
1. There should be views for employees to either login or register to system. For start
employees are being registered as Administrator

General requirements
1. All views should be displayed smoothly even to mobile devices. For this bootstrap and
collapsible components will be used (sidebar, topbar)

What we are going to use


We have all the requirements, now we need to decide the technologies we are going to use in
order to build our SPA application.

Server Side
1. ASP.NET Web API for serving data to Web clients (browsers)
2. Entity Framework as Object-relational Mapper for accessing data (SQL Server)
3. Autofac for Inversion of Control Container and resolving dependencies
4. Automapper for mapping Domain entities to ViewModels
5. FluentValidation for validating ViewModels in Web API Controllers
Front Side
1. AngularJS as the core JavaScript framework
2. Bootstrap 3 as the CSS framework for creating a fluent and mobile compatible interface
3. 3rd party libraries

SPA architecture
We have seen both application’s requirements and the technologies we are going to use, now it’s
time to design a decoupled, testable and scalable solution. There are two different designs we
need to provide here. The first one has to do with the entire project’s solution structure and how
is this divided in independent components. The second one has to do with the SPA structure
itself, that is how angularJS folders and files will be organized.
Application Design – 1
1. At the lowest level we have the Database. We ‘ll use Entity Framework Code First but
this doesn’t prevent us to design the database directly from the SQL Server. In fact that’s
what I did, I created all the tables and relationships in my SQL Server and then added the
respective model Entities. That way I didn’t have to work with Code First
Migrations and have more control on my database and entities. Though, I have to note
that when development processes finished, I enabled code first migrations and added a
seed method to initialize some data, just to help you kick of the project. We ‘ll see more
about this in the installation section.
2. The next level are the domain Entities. These are the classes that will map our database
tables. One point I want to make here is that when I started design my entities, none of
them had virtual references or collections for lazy loading. Those virtual properties were
added during the development and the needs of the application.

3. Entity Framework configurations, DbContext and Generic Repositories are the next
level. Here we ‘ll configure EF and we ‘ll create the base classes and repositories to
access database data
4. Service layer is what comes next. For this application there will be only one service
the membership service. This means that data repositories are going to be injected
directly to our Web API Controllers. I ‘ve made that decision because there will be no
complex functionality as far the data accessing. If you wish though you can use this layer
to add a middle layer for data accessing too.
5. Last but not least is the Web application that will contain the Web API Controllers and
the SPA itself. This project will start as an Empty ASP.NET Web Application with both
Web API and MVC references included.
Let’s take a look at the Database design.

Notice that for each Movie can exist multiple stock items that may be available or not. Think this
as there are many DVDs of the same movie. The movie itself will be categorized as available or
not depending on if there is any available stock item or not. Each customer rents a stock item and
when he/she does, this stock item becomes unavailable until he/she returns it back to store. The
tables used to accomplish this functionality are Customer, Rental, Stock. You can also see that
there are 3 membership tables, User, UserRole and Role which are quite self explanatory. Upon
them we ‘ll build the custom membership mechanism. I have also created an Error table just to
show you how to avoid polluting you code with Try, Catch blocks and have a centralized logging
point in your application.

Application Design – 2 (angular components)


1. Folders are organized by Feature in our SPA. This means that you ‘ll see folders such
as Customers, Movies and Rental
2. Each of those folders may have angularJS controllers, directives or templates

3. There is a folder Modules for hosting reusable components-modules. Those modules use
common directives or services from the respective common folders
4. 3rd party libraries are inside a folder Vendors. Here I want to point something
important. You should (if not already yet) start using Bower for installing-downloading
web dependencies, packages etc.. After you download required packages through Bower,
you can then either include them in your project or simple simply reference them from
their downloaded folder. In this application though, you will find all the required vendors
inside this folder and just for reference, I will provide you with the bower installation
commands for most of those packages.
Domain Entities and Data repositories
Time to start building our Single Page Application . Create a new empty solution
named HomeCinemaand add new class library project named HomeCinema.Entities. We ‘ll
create those first. All of our entities will implement an IEntityBase interface which means that
will have an ID property mapping to their primary key in the database. Add the following
interface:
IEntityBase.cs
1 public interface IEntityBase
2 {
3 int ID { get; set; }
4 }
Each movie belongs to a specific Genre (Comedy, Drama, Action, etc..). If we want to be able to
retrieve all movies through a Genre instance, then we need to add a virtual collection of Movies
property.
Genre.cs
1 public class Genre : IEntityBase
{
2 public Genre()
3 {
4 Movies = new List<Movie>();
5 }
6 public int ID { get; set; }
public string Name { get; set; }
7 public virtual ICollection<Movie> Movies { get; set; }
8 }
9
10
The most important Entity of our application is the Movie. A movie holds information such as
title, director, release date, trailer URL (Youtube) or rating. As we have already mentioned, for
each movie there are several stock items and hence for this entity we need to add a collection of
Stock.
Movie.cs
1
2
public class Movie : IEntityBase
3
{
4 public Movie()
5 {
6 Stocks = new List<Stock>();
7 }
public int ID { get; set; }
8 public string Title { get; set; }
9 public string Description { get; set; }
10 public string Image { get; set; }
11 public int GenreId { get; set; }
12 public virtual Genre Genre { get; set; }
public string Director { get; set; }
13 public string Writer { get; set; }
14 public string Producer { get; set; }
15 public DateTime ReleaseDate { get; set; }
16 public byte Rating { get; set; }
public string TrailerURI { get; set; }
17 public virtual ICollection<Stock> Stocks { get; set; }
18 }
19
20

Each stock actually describes a DVD by itself. It has a reference to a specific movie and a unique
key (code) that uniquely identifies it. For example, when there are three available DVDs for a
specific movie then 3 unique codes identify those DVDs. The employee will choose among those
codes which could probably be written on the DVD to rent a specific movie to a customer. Since
a movie rental is directly connected to a stock item, Stock entity may have a collection of Rental
items that is all rentals for this stock item.

Stock.cs
1 public class Stock : IEntityBase
2 {
public Stock()
3
{
4 Rentals = new List<Rental>();
5 }
6 public int ID { get; set; }
7 public int MovieId { get; set; }
public virtual Movie Movie { get; set; }
8 public Guid UniqueKey { get; set; }
9 public bool IsAvailable { get; set; }
10 public virtual ICollection<Rental> Rentals { get; set; }
11 }
12
13

The customer Entity is self explanatory.

Customer.cs
1
2 public class Customer : IEntityBase
3 {
public int ID { get; set; }
4 public string FirstName { get; set; }
5 public string LastName { get; set; }
6 public string Email { get; set; }
7 public string IdentityCard { get; set; }
8 public Guid UniqueKey { get; set; }
public DateTime DateOfBirth { get; set; }
9 public string Mobile { get; set; }
10 public DateTime RegistrationDate { get; set; }
11 }
12
The Rental entity which finally describes a DVD rental for a specific customer holds
information about the customer, the stock item he/she picked (DVD and its code), the rentals
date, its status (Borrowed or Returned and the date the customer returned it.
Rental.cs
1
2 public class Rental : IEntityBase
{
3 public int ID { get; set; }
4 public int CustomerId { get; set; }
5 public int StockId { get; set; }
6 public virtual Stock Stock { get; set; }
7 public DateTime RentalDate { get; set; }
public Nullable<DateTime> ReturnedDate { get; set; }
8 public string Status { get; set; }
9 }
10
Now let’s see all Entities related to Membership . The first one is the Role that describes logged
in user’s role. For our application there will be only the Admin role (employees) but we will
discuss later the scalability options we have in case we want customers to use the application as
well. Let me remind you that we are going to use Basic Authentication for Web API Controllers
and many controllers and their actions will have an Authorize attribute and a list of roles
authorized to access their resources.
Role.cs
1 public class Role : IEntityBase
2 {
3 public int ID { get; set; }
4 public string Name { get; set; }
}
5
User entity holds basic information for the user and most important the salt and the encrypted by
this salt, password.
User.cs
1
2 public class User : IEntityBase
3 {
4 public User()
5 {
UserRoles = new List<UserRole>();
6 }
7 public int ID { get; set; }
8 public string Username { get; set; }
9 public string Email { get; set; }
public string HashedPassword { get; set; }
10 public string Salt { get; set; }
11 public bool IsLocked { get; set; }
12 public DateTime DateCreated { get; set; }
13
14 public virtual ICollection<UserRole> UserRoles { get; set; }
15 }
16
A user may have more than one roles so we have a UserRole Entity as well.
1
public class UserRole :IEntityBase
2 {
3 public int ID { get; set; }
4 public int UserId { get; set; }
5 public int RoleId { get; set; }
public virtual Role Role { get; set; }
6 }
7
One last entity I have added is the Error. It is always good to log your application’s errors and
we ‘ll use a specific repository to do this. I decided to add error logging functionality in order to
show you a nice trick that will prevent you from polluting you controllers with Try Catch blocks
all over the place. We ‘ll see it in action when we reach Web API Controllers.
Error.cs
1
public class Error : IEntityBase
2 {
3 public int ID { get; set; }
4 public string Message { get; set; }
5 public string StackTrace { get; set; }
public DateTime DateCreated { get; set; }
6 }
7

Data Repositories
Add a new class library project named HomeCinema.Data and add reference
to HomeCinema.Entitiesproject. Make sure you also install Entity Framework through Nuget
Packages. For start we will create EF Configurations for our Entities. Add a new folder
named Configurations and add the following configuration to declare the primary key for our
Entities:
EntityBaseConfiguration.cs
1 public class EntityBaseConfiguration<T> : EntityTypeConfiguration<T> where T : class, I
2 {
3 public EntityBaseConfiguration()
{
4 HasKey(e => e.ID);
5 }
6 }
7

Entity Framework either way assumes that a property named “ID” is a primary key but this is a
nice way to declare it in case you give this property different name. Following are one by one all
other configurations. I will highlight the important lines (if any) to notice for each of these.

GenreConfiguration.cs
1 public class GenreConfiguration :
EntityBaseConfiguration<Genre>
2 {
3 public GenreConfiguration()
4 {
5 Property(g =>
g.Name).IsRequired().HasMaxLength(50);
6 }
7 }
MovieConfiguration.cs
public MovieConfiguration()
{
Property(m =>
m.Title).IsRequired().HasMaxLength(100);
1 Property(m => m.GenreId).IsRequired();
2 Property(m =>
3 m.Director).IsRequired().HasMaxLength(100);
4 Property(m =>
5 m.Writer).IsRequired().HasMaxLength(50);
Property(m =>
6 m.Producer).IsRequired().HasMaxLength(50);
7 Property(m =>
8 m.Writer).HasMaxLength(50);
9 Property(m =>
m.Producer).HasMaxLength(50);
10 Property(m => m.Rating).IsRequired();
11 Property(m =>
12 m.Description).IsRequired().HasMaxLength(2000);
13 Property(m =>
14 m.TrailerURI).HasMaxLength(200);
HasMany(m =>
15 m.Stocks).WithRequired().HasForeignKey(s =>
s.MovieId);
}
}
StockConfiguration.cs
1 public class StockConfiguration : EntityBaseConfiguration<Stock>
2 {
public StockConfiguration()
3 {
4 Property(s => s.MovieId).IsRequired();
5 Property(s => s.UniqueKey).IsRequired();
6 Property(s => s.IsAvailable).IsRequired();
7 HasMany(s => s.Rentals).WithRequired(r=> r.Stock).HasForeignKey(r => r.St
}
8 }
9
10
CustomerConfiguration.cs
1
2 public class CustomerConfiguration : EntityBaseConfiguration<Customer>
3 {
4 public CustomerConfiguration()
{
5 Property(u => u.FirstName).IsRequired().HasMaxLength(100);
6 Property(u => u.LastName).IsRequired().HasMaxLength(100);
7 Property(u => u.IdentityCard).IsRequired().HasMaxLength(50);
8 Property(u => u.UniqueKey).IsRequired();
Property(c => c.Mobile).HasMaxLength(10);
9 Property(c => c.Email).IsRequired().HasMaxLength(200);
10 Property(c => c.DateOfBirth).IsRequired();
11 }
12 }
13
RentalConfiguration.cs
1
2 public class RentalConfiguration : EntityBaseConfiguration<Rental>
{
3 public RentalConfiguration()
4 {
5 Property(r => r.CustomerId).IsRequired();
6 Property(r => r.StockId).IsRequired();
7 Property(r => r.Status).IsRequired().HasMaxLength(10);
Property(r => r.ReturnedDate).IsOptional();
8 }
9 }
10
RoleConfiguration.cs
1
public class RoleConfiguration : EntityBaseConfiguration<Role>
2 {
3 public RoleConfiguration()
4 {
5 Property(ur => ur.Name).IsRequired().HasMaxLength(50);
}
6 }
7
UserRoleConfiguration.cs
1 public class UserRoleConfiguration :
EntityBaseConfiguration<UserRole>
2 {
3 public UserRoleConfiguration()
4 {
5 Property(ur =>
ur.UserId).IsRequired();
6 Property(ur =>
7 ur.RoleId).IsRequired();
8 }
}
UserConfiguration.cs
public class UserConfiguration :
EntityBaseConfiguration<User>
1 {
2 public UserConfiguration()
3 {
Property(u =>
4 u.Username).IsRequired().HasMaxLength(100);
5 Property(u =>
6 u.Email).IsRequired().HasMaxLength(200);
7 Property(u =>
8 u.HashedPassword).IsRequired().HasMaxLength(200);
Property(u =>
9 u.Salt).IsRequired().HasMaxLength(200);
10 Property(u =>
11 u.IsLocked).IsRequired();
12 Property(u => u.DateCreated);
}
}
Those configurations will affect how the database tables will be created. Add a new class
named HomeCinemaContext at the root of the project. This class will inherit
from DbContext and will be the main class for accessing data from the database.
1 public class HomeCinemaContext : DbContext
2 {
public HomeCinemaContext()
3 : base("HomeCinema")
4 {
5 Database.SetInitializer<HomeCinemaContext>(null);
6 }
7
#region Entity Sets
8 public IDbSet<User> UserSet { get; set; }
9 public IDbSet<Role> RoleSet { get; set; }
10 public IDbSet<UserRole> UserRoleSet { get; set; }
11 public IDbSet<Customer> CustomerSet { get; set; }
12 public IDbSet<Movie> MovieSet { get; set; }
public IDbSet<Genre> GenreSet { get; set; }
13 public IDbSet<Stock> StockSet { get; set; }
14 public IDbSet<Rental> RentalSet { get; set; }
15 public IDbSet<Error> ErrorSet { get; set; }
16 #endregion
17
public virtual void Commit()
18 {
19 base.SaveChanges();
20 }
21 protected override void OnModelCreating(DbModelBuilder modelBuilder)
22 {
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
23
24 modelBuilder.Configurations.Add(new UserConfiguration());
25 modelBuilder.Configurations.Add(new UserRoleConfiguration());
26 modelBuilder.Configurations.Add(new RoleConfiguration());
27 modelBuilder.Configurations.Add(new CustomerConfiguration());
28 modelBuilder.Configurations.Add(new MovieConfiguration());
modelBuilder.Configurations.Add(new GenreConfiguration());
29 modelBuilder.Configurations.Add(new StockConfiguration());
30 modelBuilder.Configurations.Add(new RentalConfiguration());
31 }
32 }
33
34
35
36
37
38
Notice that I ‘ve made a decision to name all of the Entity Sets with a Set prefix. Also I turned of
the default pluralization convention that Entity Framework uses when creating the tables in
database. This will result in tables having the same name as the Entity. You need to add
an App.config file if not exists and create the following connection string:
App.config con
1<connectionStrings>
<add name="HomeCinema" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=Hom
2providerName="System.Data.SqlClient" />
3</connectionStrings>
You can alter the server if you wish to match your development environment.
Let us proceed with the UnitOfWork pattern implementation. Add a folder
named Infrastructure and paste the following classes and interfaces:
Disposable.cs
1 public class Disposable : IDisposable
{
2
private bool isDisposed;
3
4 ~Disposable()
5 {
6 Dispose(false);
7 }
8
public void Dispose()
9 {
10 Dispose(true);
11 GC.SuppressFinalize(this);
12 }
13 private void Dispose(bool disposing)
{
14 if (!isDisposed && disposing)
15 {
16 DisposeCore();
17 }
18
isDisposed = true;
19 }
20
21 // Ovveride this to dispose custom objects
22 protected virtual void DisposeCore()
23 {
24 }
}
25
26
27
28
29
IDbFactory.cs
1 public interface IDbFactory : IDisposable
2 {
3 HomeCinemaContext Init();
4 }
DbFactory.cs
1
2 public class DbFactory : Disposable, IDbFactory
3 {
4 HomeCinemaContext dbContext;
5
public HomeCinemaContext Init()
6 {
7 return dbContext ?? (dbContext = new HomeCinemaContext());
8 }
9
10 protected override void DisposeCore()
{
11 if (dbContext != null)
12 dbContext.Dispose();
13 }
14 }
15
IUnitOfWork.cs
1 public interface IUnitOfWork
2 {
3 void Commit();
4 }
UnitOfWork.cs
public class UnitOfWork : IUnitOfWork
1 {
2 private readonly IDbFactory dbFactory;
3 private HomeCinemaContext dbContext;
4
5 public UnitOfWork(IDbFactory dbFactory)
{
6 this.dbFactory = dbFactory;
7 }
8
9 public HomeCinemaContext DbContext
10 {
get { return dbContext ?? (dbContext = dbFactory.Init()); }
11 }
12
13 public void Commit()
14 {
DbContext.Commit();
15 }
16 }
17
18
19
20
Time for the Generic Repository Pattern . We have seen this pattern many times in this blog but
this time I will make a slight change. One of the blog’s readers asked me if he had to create a
specific repository class that implements the generic repository T each time a need for a new
type of repository is needed. Reader’s question was really good if you think that you may have
hundred of Entities in a large scale application. The answer is NO and we will see it on action in
this project where will try to inject repositories of type T as needed. Create a folder
named Repositories and add the following interface with its implementation class:
IEntityBaseRepository.cs
1
2 public interface IEntityBaseRepository<T> where T : class, IEntityBase, new()
{
3
IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties
4 IQueryable<T> All { get; }
5 IQueryable<T> GetAll();
6 T GetSingle(int id);
7 IQueryable<T> FindBy(Expression<Func<T, bool>> predicate);
void Add(T entity);
8 void Delete(T entity);
9 void Edit(T entity);
10 }
11
EntityBaseRepository.cs
1 public class EntityBaseRepository<T> : IEntityBaseRepository<T>
2 where T : class, IEntityBase, new()
{
3
4 private HomeCinemaContext dataContext;
5
6 #region Properties
7 protected IDbFactory DbFactory
8 {
9 get;
private set;
10 }
11
12 protected HomeCinemaContext DbContext
13 {
14 get { return dataContext ?? (dataContext = DbFactory.Init()); }
}
15
public EntityBaseRepository(IDbFactory dbFactory)
16 {
17 DbFactory = dbFactory;
18 }
19 #endregion
20 public virtual IQueryable<T> GetAll()
{
21 return DbContext.Set<T>();
22 }
23 public virtual IQueryable<T> All
24 {
get
25 {
26 return GetAll();
27 }
28 }
29 public virtual IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] inc
{
30 IQueryable<T> query = DbContext.Set<T>();
31 foreach (var includeProperty in includeProperties)
32 {
33 query = query.Include(includeProperty);
}
34 return query;
35 }
36 public T GetSingle(int id)
37 {
38 return GetAll().FirstOrDefault(x => x.ID == id);
}
39 public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
40 {
41 return DbContext.Set<T>().Where(predicate);
42 }
43
44 public virtual void Add(T entity)
{
45 DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity);
46 DbContext.Set<T>().Add(entity);
47 }
48 public virtual void Edit(T entity)
{
49 DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity);
50 dbEntityEntry.State = EntityState.Modified;
51 }
52 public virtual void Delete(T entity)
53 {
DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity);
54 dbEntityEntry.State = EntityState.Deleted;
55 }
56 }
57
58
59
60
61
62
63
64
65
66
67
Before leaving this project and proceed with the Services one, there is only one thing remained to
do. As I said when I was developing this application I was designing the database on my SQL
Server and adding the respective Entities at the same time (yeap, you can do this as well..). No
migrations where enabled. Yet, I thought that I should enable them in order to help you kick of
the project and create the database automatically. For this you should do the same thing if you
follow along with me. Open Package Manager Console , make sure you have selected
the HomeCinema.Data project and type the following command:
1 enable-migrations
This will add a Configuration class inside a Migrations folder. This Configuration class has a
seed method that is invoked when you create the database. The seed method I ‘ve written is a
little bit large for pasting it here so please find it here. What I did is add data for Genres, Movies,
Roles, Customers and Stocks. For customers I used a Nuget Package named MockData so make
sure you install it too. More over I added a user with username chsakell and
password homecinema. You can use them in order to login to our SPA. Otherwise you can just
register a new user and sign in with those credentials. If you want to create the database right
now, run the following commands from the Package Manager Console:
1 add-migration "initial_migration"
1 update-database -verbose
We ‘ll come up again to HomeCinema.Data project later to add some extension methods.

Membership
There is one middle layer between the Web application and the Data repositories and that’s
the Service layer. In this application though, we will use this layer only for the membership’s
requirements leaving all data repositories being injected as they are directly to API Controllers .
Add a new class library project named HomeCinema.Services and make sure you add references
to both of the other projects, HomeCinema.Data and HomeCinema.Entities. First, we ‘ll create a
simple Encryption service to create salts and encrypted passwords and then we ‘ll use this
service to implement a custom membership mechanism. Add a folder named Abstract and create
the following interfaces.
IEncryptionService.cs
1public interface IEncryptionService
2 {
3 string CreateSalt();
4 string EncryptPassword(string password, string salt);
}
5
IMembershipService.cs
1public interface IMembershipService
2 {
MembershipContext ValidateUser(string username, string password);
3 User CreateUser(string username, string email, string password, int[] roles);
4 User GetUser(int userId);
5 List<Role> GetUserRoles(string username);
6 }
7
At the root of this project add the EncryptionService implementation. It is a simple password
encryption based on a salt and the SHA256 algorithm
from System.Security.Cryptography namespace. Of course you can always use your own
implementation algorithm.
EncryptionService.cs
1
2
3 public class EncryptionService : IEncryptionService
{
4 public string CreateSalt()
5 {
6 var data = new byte[0x10];
7 using (var cryptoServiceProvider = new RNGCryptoServiceProvider())
{
8
cryptoServiceProvider.GetBytes(data);
9 return Convert.ToBase64String(data);
10 }
11 }
12
13 public string EncryptPassword(string password, string salt)
{
14 using (var sha256 = SHA256.Create())
15 {
16 var saltedPassword = string.Format("{0}{1}", salt, password);
17 byte[] saltedPasswordAsBytes = Encoding.UTF8.GetBytes(saltedPassword)
18 return Convert.ToBase64String(sha256.ComputeHash(saltedPasswordAsByte
}
19 }
20 }
21
22

Let’s move to the Membership Service now. For start let’s see the base components of this class.
Add the following class at the root of the project as well.

MembershipService.cs
1 public class MembershipService : IMembershipService
{
2 #region Variables
3 private readonly IEntityBaseRepository<User> _userRepository;
4 private readonly IEntityBaseRepository<Role> _roleRepository;
5 private readonly IEntityBaseRepository<UserRole> _userRoleRepository;
private readonly IEncryptionService _encryptionService;
6 private readonly IUnitOfWork _unitOfWork;
7 #endregion
8
9 public MembershipService(IEntityBaseRepository<User> userRepository, IEntityB
10 IEntityBaseRepository<UserRole> userRoleRepository, IEncryptionService encryp
11 {
_userRepository = userRepository;
12 _roleRepository = roleRepository;
13 _userRoleRepository = userRoleRepository;
14 _encryptionService = encryptionService;
15 _unitOfWork = unitOfWork;
}
16 }
17
18
19
20
Here we can see for the first time the way the generic repositories are going to be injected
through the Autofac Inversion of Control Container. Before moving to the core implementation
of this service we will have to add a User extension method in HomeCinema.Data and some
helper methods in the previous class. Switch to HomeCinema.Data and add a new folder
named Extensions. We will use this folder for adding Data repository extensions based on the
Entity Set. Add the following User Entity extension method which retrieves a User instance
based on its username.
UserExtensions.cs
1
public static class UserExtensions
2 {
3 public static User GetSingleByUsername(this IEntityBaseRepository<User> userRep
4 {
5 return userRepository.GetAll().FirstOrDefault(x => x.Username == username)
}
6 }
7
Switch again to MembershipService class and add the following helper private methods.
MembershipService helper methods
#region Helper methods
1 private void addUserToRole(User user, int roleId)
2 {
3 var role = _roleRepository.GetSingle(roleId);
4 if (role == null)
throw new ApplicationException("Role doesn't exist.");
5
6 var userRole = new UserRole()
7 {
8 RoleId = role.ID,
9 UserId = user.ID
};
10 _userRoleRepository.Add(userRole);
11 }
12
13 private bool isPasswordValid(User user, string password)
14 {
15 return string.Equals(_encryptionService.EncryptPassword(password, user.Sa
}
16
17 private bool isUserValid(User user, string password)
18 {
19 if (isPasswordValid(user, password))
20 {
return !user.IsLocked;
21
}
22
23 return false;
24 }
#endregion
25
26
27
28
29
30
Let’s view the CreateUser implementation method. The method checks if username already in
use and if not creates the user.
CreateUser method
1
2 public User CreateUser(string username, string email, string password, int[] roles)
3 {
4 var existingUser = _userRepository.GetSingleByUsername(username);
5
6 if (existingUser != null)
7 {
throw new Exception("Username is already in use");
8 }
9
10 var passwordSalt = _encryptionService.CreateSalt();
11
12 var user = new User()
13 {
Username = username,
14
Salt = passwordSalt,
15 Email = email,
16 IsLocked = false,
17 HashedPassword = _encryptionService.EncryptPassword(password, passwor
18 DateCreated = DateTime.Now
};
19
20 _userRepository.Add(user);
21
22 _unitOfWork.Commit();
23
24 if (roles != null || roles.Length > 0)
25 {
26 foreach (var role in roles)
{
27 addUserToRole(user, role);
28 }
29 }
30
31 _unitOfWork.Commit();
32
return user;
33 }
34
35
36
37
The GetUser and GetUserRoles implementations are quite simple.
GetUser method
1 public User GetUser(int userId)
{
2 return
3 _userRepository.GetSingle(userId);
4 }
GetUserRoles method
1
public List<Role> GetUserRoles(string username)
2 {
3 List<Role> _result = new List<Role>();
4
5 var existingUser =
6 _userRepository.GetSingleByUsername(username);
7
8 if (existingUser != null)
{
9 foreach (var userRole in existingUser.UserRoles)
10 {
11 _result.Add(userRole.Role);
12 }
}
13
14 return _result.Distinct().ToList();
15 }
16
I left the ValidateUser implementation last because is a little more complex than the others. You
will have noticed from its interface that this service make use of a class
named MembershipContext. This custom class is the one that will hold the IPrincipal object
when authenticating users. When a validuser passes his/her credentials the service method will
create an instance of GenericIdentity for user’s username. Then it will set the IPrincipal property
using the GenericIdentity created and user’s roles. User roles information will be used to
authorize API Controller’s actions based on logged in user’s roles. Let’s see
the MembershipContext class and then the ValidateUser implementation.
MembershipContext.cs
1
public class MembershipContext
2
{
3 public IPrincipal Principal { get; set; }
4 public User User { get; set; }
5 public bool IsValid()
6 {
return Principal != null;
7 }
8 }
9
ValidateUser method
1 public MembershipContext ValidateUser(string username, string password)
2 {
3 var membershipCtx = new MembershipContext();
4
5 var user = _userRepository.GetSingleByUsername(username);
if (user != null && isUserValid(user, password))
6 {
7 var userRoles = GetUserRoles(user.Username);
8 membershipCtx.User = user;
9
10 var identity = new GenericIdentity(user.Username);
membershipCtx.Principal = new GenericPrincipal(
11 identity,
12 userRoles.Select(x => x.Name).ToArray());
13 }
14
15 return membershipCtx;
16 }
17
18
Check that MembershipContext class may hold additional information for the User. Add
anything else you wish according to your needs.

Single Page Application


We are done building the core components for the HomeCinema Single Page Application, now
it’s time to create the Web Application that will make use all of the previous parts we created.
Add a new Empty Web Application project named HomeCinema.Web and make sure to check
both the Web APIand MVC check buttons. Add references to all the previous projects and install
the following Nuget Packages:
Nuget Packages
1. Entity Framework

2. Autofac ASP.NET Web API 2.2 Integration

3. Automapper

4. FluentValidation
First thing we need to do is create any configurations we want to apply to our application. We ‘ll
start with the Autofac Inversion of Control Container to work along with Web API framework.
Next we will configure the bundling and last but not least, we will add the MessageHandler to
configure the Basic Authentication. Add the following class inside the App_Start project’s
folder.
AutofacWebapiConfig.cs
public class AutofacWebapiConfig
1 {
2 public static IContainer Container;
3 public static void Initialize(HttpConfiguration config)
4 {
Initialize(config, RegisterServices(new ContainerBuilder()));
5 }
6
7 public static void Initialize(HttpConfiguration config, IContainer container)
8 {
config.DependencyResolver = new AutofacWebApiDependencyResolver(container
9 }
10
11 private static IContainer RegisterServices(ContainerBuilder builder)
12 {
13 builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
14
// EF HomeCinemaContext
15 builder.RegisterType<HomeCinemaContext>()
16 .As<DbContext>()
17 .InstancePerRequest();
18
19 builder.RegisterType<DbFactory>()
20 .As<IDbFactory>()
.InstancePerRequest();
21
22 builder.RegisterType<UnitOfWork>()
23 .As<IUnitOfWork>()
24 .InstancePerRequest();
25
26 builder.RegisterGeneric(typeof(EntityBaseRepository<>))
.As(typeof(IEntityBaseRepository<>))
27 .InstancePerRequest();
28
29 // Services
30 builder.RegisterType<EncryptionService>()
31 .As<IEncryptionService>()
32 .InstancePerRequest();
33
builder.RegisterType<MembershipService>()
34 .As<IMembershipService>()
35 .InstancePerRequest();
36
37 Container = builder.Build();
38
39 return Container;
40 }
}
41
42
43
44
45
46
47
48
This will make sure dependencies will be injected in constructors as expected. Since we are in
the App_Start folder let’s configure the Bundling in our application. Add the following class in
App_Start folder as well.

BundleConfig.cs
1 public class BundleConfig
2 {
3 public static void RegisterBundles(BundleCollection bundles)
{
4 bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
5 "~/Scripts/Vendors/modernizr.js"));
6
7 bundles.Add(new ScriptBundle("~/bundles/vendors").Include(
8 "~/Scripts/Vendors/jquery.js",
9 "~/Scripts/Vendors/bootstrap.js",
"~/Scripts/Vendors/toastr.js",
10 "~/Scripts/Vendors/jquery.raty.js",
11 "~/Scripts/Vendors/respond.src.js",
12 "~/Scripts/Vendors/angular.js",
13 "~/Scripts/Vendors/angular-route.js",
"~/Scripts/Vendors/angular-cookies.js",
14
"~/Scripts/Vendors/angular-validator.js",
15 "~/Scripts/Vendors/angular-base64.js",
16 "~/Scripts/Vendors/angular-file-upload.js",
17 "~/Scripts/Vendors/angucomplete-alt.min.js",
18 "~/Scripts/Vendors/ui-bootstrap-tpls-0.13.1.js",
"~/Scripts/Vendors/underscore.js",
19 "~/Scripts/Vendors/raphael.js",
20 "~/Scripts/Vendors/morris.js",
21 "~/Scripts/Vendors/jquery.fancybox.js",
22 "~/Scripts/Vendors/jquery.fancybox-media.js",
23 "~/Scripts/Vendors/loading-bar.js"
));
24
25 bundles.Add(new ScriptBundle("~/bundles/spa").Include(
26 "~/Scripts/spa/modules/common.core.js",
27 "~/Scripts/spa/modules/common.ui.js",
28 "~/Scripts/spa/app.js",
"~/Scripts/spa/services/apiService.js",
29 "~/Scripts/spa/services/notificationService.js",
30 "~/Scripts/spa/services/membershipService.js",
31 "~/Scripts/spa/services/fileUploadService.js",
32 "~/Scripts/spa/layout/topBar.directive.js",
33 "~/Scripts/spa/layout/sideBar.directive.js",
"~/Scripts/spa/layout/customPager.directive.js",
34 "~/Scripts/spa/directives/rating.directive.js",
35 "~/Scripts/spa/directives/availableMovie.directive.js",
36 "~/Scripts/spa/account/loginCtrl.js",
37 "~/Scripts/spa/account/registerCtrl.js",
"~/Scripts/spa/home/rootCtrl.js",
38 "~/Scripts/spa/home/indexCtrl.js",
39 "~/Scripts/spa/customers/customersCtrl.js",
40 "~/Scripts/spa/customers/customersRegCtrl.js",
41 "~/Scripts/spa/customers/customerEditCtrl.js",
42 "~/Scripts/spa/movies/moviesCtrl.js",
43 "~/Scripts/spa/movies/movieAddCtrl.js",
44 "~/Scripts/spa/movies/movieDetailsCtrl.js",
"~/Scripts/spa/movies/movieEditCtrl.js",
45 "~/Scripts/spa/controllers/rentalCtrl.js",
46 "~/Scripts/spa/rental/rentMovieCtrl.js",
47 "~/Scripts/spa/rental/rentStatsCtrl.js"
48 ));
49
bundles.Add(new StyleBundle("~/Content/css").Include(
50 "~/content/css/site.css",
51 "~/content/css/bootstrap.css",
52 "~/content/css/bootstrap-theme.css",
53 "~/content/css/font-awesome.css",
54 "~/content/css/morris.css",
"~/content/css/toastr.css",
55 "~/content/css/jquery.fancybox.css",
56 "~/content/css/loading-bar.css"));
57
58 BundleTable.EnableOptimizations = false;
59 }
}
60
61
62
63
64
65
66
67
68
69
70
71
You may wonder where the heck will I find all these files? Do not worry about that, i will
provide you links for all the static JavaScript libraries inside the ~/bundles/vendors bundle and
the CSS stylesheets in the ~/Content/css one as well. All JavaScript files in
the ~/bundles/spa bundle are the angularJS components we are going to build. Don’t forget that
as always the source code for this application will be available at the end of this post. Add
the Bootstrapper class in the App_Start folder. We will call its Run method from the Global.asax
ApplicationStart method. For now let the Automapper’s part commented out and we will un-
comment it when the time comes.
Bootstrapper.cs
1 public class Bootstrapper
{
2 public static void Run()
3 {
4 // Configure Autofac
5 AutofacWebapiConfig.Initialize(GlobalConfiguration.Configuration);
6 //Configure AutoMapper
//AutoMapperConfiguration.Configure();
7 }
8 }
9
10
Change Global.asax.cs (add it if not exists) as follow:
Global.cs
1
2 public class Global : HttpApplication
3 {
4 void Application_Start(object sender, EventArgs e)
{
5 var config = GlobalConfiguration.Configuration;
6
7 AreaRegistration.RegisterAllAreas();
8 WebApiConfig.Register(config);
9 Bootstrapper.Run();
RouteConfig.RegisterRoutes(RouteTable.Routes);
10 GlobalConfiguration.Configuration.EnsureInitialized();
11 BundleConfig.RegisterBundles(BundleTable.Bundles);
12 }
13 }
14
Not all pages will be accessible to unauthenticated users as opposed from application
requirements and for this reason we are going to use Basic Authentication. This will be done
through a Message Handler whose job is to search for an Authorization header in the request.
Create a folder named Infrastructure at the root of the web application project and add a sub-
folder named MessageHandlers. Create the following handler.
HomeCinemaAuthHandler.cs
public class HomeCinemaAuthHandler : DelegatingHandler
1 {
2 IEnumerable<string> authHeaderValues = null;
3 protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage requ
4 {
5 try
{
6 request.Headers.TryGetValues("Authorization",out authHeaderValues);
7 if(authHeaderValues == null)
8 return base.SendAsync(request, cancellationToken); // cross finge
9
10 var tokens = authHeaderValues.FirstOrDefault();
tokens = tokens.Replace("Basic","").Trim();
11 if (!string.IsNullOrEmpty(tokens))
12 {
13 byte[] data = Convert.FromBase64String(tokens);
14 string decodedString = Encoding.UTF8.GetString(data);
15 string[] tokensValues = decodedString.Split(':');
var membershipService = request.GetMembershipService();
16
17 var membershipCtx = membershipService.ValidateUser(tokensValues[0
18 if (membershipCtx.User != null)
19 {
20 IPrincipal principal = membershipCtx.Principal;
Thread.CurrentPrincipal = principal;
21
HttpContext.Current.User = principal;
22 }
23 else // Unauthorized access - wrong crededentials
24 {
var response = new HttpResponseMessage(HttpStatusCode.Unauthor
25 var tsc = new TaskCompletionSource<HttpResponseMessage>();
26 tsc.SetResult(response);
27 return tsc.Task;
28 }
}
29 else
30 {
31 var response = new HttpResponseMessage(HttpStatusCode.Forbidden);
32 var tsc = new TaskCompletionSource<HttpResponseMessage>();
33 tsc.SetResult(response);
return tsc.Task;
34 }
35 return base.SendAsync(request, cancellationToken);
36 }
37 catch
{
38 var response = new HttpResponseMessage(HttpStatusCode.Forbidden);
39 var tsc = new TaskCompletionSource<HttpResponseMessage>();
40 tsc.SetResult(response);
41 return tsc.Task;
42 }
}
43 }
44
45
46
47
48
49
50
51
52
53
The GetMembershipService() is an HttpRequestMessage extension which I will provide right
away. Notice that we ‘ve made some decisions in this handler. When a request dispatches, the
handler searches for an Authorization header. If it doesn’t find one then we cross our fingers and
let the next level decide if the request is accessible or not. This means that if a Web API
Controller’s action has the AllowAnonymous attribute the request doesn’t have to hold an
Authorization header. One the other hand if the action did have an Authorize attribute then
an Unauthorized response message would be returned in case of empty Authorization header. If
Authorization header is present, the membership service decodes the based64 encoded
credentials and checks their validity. User and role information is saved in
the HttpContext.Current.User object.
As far as the HttpRequestMessage extension add a folder named Extensions inside the
Infrastructure folder and create the following class.
RequestMessageExtensions.cs
1
2 public static class RequestMessageExtensions
3 {
internal static IMembershipService GetMembershipService(this HttpRequestMessag
4
{
5 return request.GetService<IMembershipService>();
6 }
7
8 private static TService GetService<TService>(this HttpRequestMessage request)
9 {
IDependencyScope dependencyScope = request.GetDependencyScope();
10 TService service = (TService)dependencyScope.GetService(typeof(TService))
11
12 return service;
13 }
14 }
15
Now switch to the WebApiConfig.cs inside the App_Start folder and register the authentication
message handler.
WebApiConfig.cs
1 public static class WebApiConfig
{
2 public static void Register(HttpConfiguration config)
3 {
4 // Web API configuration and services
5 config.MessageHandlers.Add(new HomeCinemaAuthHandler());
6
7 // Web API routes
config.MapHttpAttributeRoutes();
8
9 config.Routes.MapHttpRoute(
10 name: "DefaultApi",
11 routeTemplate: "api/{controller}/{id}",
12 defaults: new { id = RouteParameter.Optional }
);
13 }
14 }
15
16
17
Don’t forget to add the same connection string you added in the HomeCinema.Data App.config
file, in the Web.config configuration file.

Static CSS files and images


We configured previously the bundling but at this point you don’t have all the necessary files and
their respective folders in your application. Add a Content folder at the root of the web
application and create the following sub-folders in it.

CSS – Fonts – Images


1. Content/css: Bootstrap, Font-awesome, fancy-box, morris, toastr, homecinema relative
css files
2. Content/fonts: Bootstrap and font-awesome required fonts
3. Content/images
4. Content/images/movies: Hold’s application’s movies
5. Content/images/raty: Raty.js required images
You can find and download all these static files from here.

Vendors – 3rd party libraries


Create a Scripts folder in application’s root and a two sub-
folders, Scripts/spa and Scripts/vendors. Each of those libraries solves a specific requirement
and it was carefully picked. Before providing you some basic information for most of them, let
me point out something important.
Always pick 3rd libraries carefully. When searching for a specific component you may find
several implementations for it out on the internet. One thing to care about is do not end up with a
bazooka while trying to kill a mosquito. For example, in this application we need modal popups.
If you search on the internet you will find various implementations but most of these are quite
complex. All we need is a modal window so for this I decided that the $modal service of
angularJS UI-bootstrap is more than enough to do the job. Another thing that you should do is
always check for any opened issues for the 3rd library. Those libraries are usually hosted
on Github and there is an Issues section that you can view. Check if any performance or memory
leaks issue exists before decide to attach any library to your project.
3rd party libraries
1. toastr.js: A non-blocking notification library
2. jquery.raty.js: A star rating plug-in
3. angular-validator.js: A light weighted validation directive
4. angular-base64.js: Base64 encode-decode library
5. angular-file-upload.js: A file-uploading library
6. angucomplete-alt.min.js: Auto-complete search directive
7. ui-bootstrap-tpls-0.13.1.js: Native AngularJS (Angular) directives for Bootstrap
8. morris.js: Charts library
9. jquery.fancybox.js: Tool for displaying images
These are the most important libraries we will be using but you may find some other files too.
Download all those files from here. As I mentioned, generally you should use Bower and Grunt
or Gulp for resolving web dependencies so let me provide you some installation commands for
the above libraries:
Bower installations
1
bower install angucomplete-alt --save
2 bower install angular-base64
3 bower install angular-file-upload
4 bower install tg-angular-validator
5 bower install bootstrap
6 bower install raty
bower install angular-loading-bar
7 bower install angular-bootstrap
8

The ng-view
Now that we are done configurating the Single Page Application it’s time to create its initial
page, the page that will be used as the parent of all different views and templates in our
application. Inside the Controllers folder, create an MVC Controller named HomeController as
follow. Right click inside its Index method and create a new view with the respective name.
HomeController.cs
1
public class HomeController : Controller
2 {
3 public ActionResult Index()
4 {
5 return View();
}
6 }
7
Alter the Views/Home/Index.cshtml file as follow:
Index.c
1 @{
2 Layout = null;
3 }
4
5 <!DOCTYPE html>
6
7 <html ng-app="homeCinema">
8 <head>
<meta charset="utf-8" />
9 <meta name="viewport" content="width=device-width, initial-scale=1.0">
10 <title>Home Cinema</title>
11 @Styles.Render("~/Content/css")
12 @Scripts.Render("~/bundles/modernizr")
</head>
13 <body ng-controller="rootCtrl">
14 <top-bar></top-bar>
15 <div class="row-offcanvas row-offcanvas-left">
16 <side-bar></side-bar>
17 <div id="main">
<div class="col-md-12">
18 <p class="visible-xs">
19 <button type="button" class="btn btn-primary btn-xs" data-toggle="
20 </p>
21 <div class="page {{ pageClass }}" ng-view></div>
</div>
22 </div>
23 </div><!--/row-offcanvas -->
24 @Scripts.Render("~/bundles/vendors")
25 @Scripts.Render("~/bundles/spa")
26
27 </body>
</html>
28
29
30
31
32
Let’s explain one by one the highlighted lines, from top to bottom. The main module in our spa
will be named homeCinema. This module will have two other modules as dependencies,
the Common.coreand the Common.UI which we will create later. Next we render the CSS
bundles we created before. There will be one root-parent controller in the application
named rootCtrl. The top-bar and side-barelements are custom angularJS directives what will
build for the top and side bar collapsible menus respectively. We use a page-
class $scope variable to change the style in a rendered template. This is a nice trick to render
different styles in ng-view templates. Last but not least, we render the vendorsand spa JavaScript
bundles.
From what we have till now, it’s obvious that first we should create the homeCinema module,
the rootCtrl controller then the two custom directives for this initial page to be rendered. Instead
of doing this, we ‘ll take a step back and prepare some basic angularJS components,
the Common.core and Common.ui modules. Add a Modules folder in the Scripts/spa folder and
create the following two files.
spa/modules/common.ui.js
1 (function () {
'use strict';
2
3 angular.module('common.ui', ['ui.bootstrap',
4'chieffancypants.loadingBar']);
5
6})();
spa/modules/common.core.js
1(function () {
'use strict';
2
3 angular.module('common.core', ['ngRoute', 'ngCookies', 'base64', 'angularFileUpload',
4'angularValidator', 'angucomplete-alt']);
5
6})();
Let Common.ui module be a UI related reusable component through our SPA application
and Common.core a core functional one. We could just inject all those dependencies directly to
the homeCinema module but that would require to do the same in case we wanted to scale the
application as we ‘ll discuss later. Pay some attention the way we created the modules. We will
be using this pattern a lot for not polluting the global JavaScript’s namespace. At the root of the
spa folder add the following app.js file and define the core homeCinema module and its routes.
spa/app.js
(function () {
1 'use strict';
2
3 angular.module('homeCinema', ['common.core', 'common.ui'])
4 .config(config);
5
6 config.$inject = ['$routeProvider'];
function config($routeProvider) {
7 $routeProvider
8 .when("/", {
9 templateUrl: "scripts/spa/home/index.html",
10 controller: "indexCtrl"
11 })
.when("/login", {
12 templateUrl: "scripts/spa/account/login.html",
13 controller: "loginCtrl"
14 })
15 .when("/register", {
templateUrl: "scripts/spa/account/register.html",
16 controller: "registerCtrl"
17 })
18 .when("/customers", {
19 templateUrl: "scripts/spa/customers/customers.html",
20 controller: "customersCtrl"
})
21 .when("/customers/register", {
22 templateUrl: "scripts/spa/customers/register.html",
23 controller: "customersRegCtrl"
24 })
.when("/movies", {
25
templateUrl: "scripts/spa/movies/movies.html",
26 controller: "moviesCtrl"
27 })
28 .when("/movies/add", {
29 templateUrl: "scripts/spa/movies/add.html",
controller: "movieAddCtrl"
30 })
31 .when("/movies/:id", {
32 templateUrl: "scripts/spa/movies/details.html",
controller: "movieDetailsCtrl"
33 })
34 .when("/movies/edit/:id", {
35 templateUrl: "scripts/spa/movies/edit.html",
36 controller: "movieEditCtrl"
})
37 .when("/rental", {
38 templateUrl: "scripts/spa/rental/rental.html",
39 controller: "rentStatsCtrl"
40 }).otherwise({ redirectTo: "/" });
41 }
42
})();
43
44
45
46
47
48
49
50
51
52
Once again take a look at the explicit service injection using the angularJS property
annotation $inject which allows the minifiers to rename the function parameters and still be
able to inject the right services. You probably don’t have all the referenced files in the previous
script (except if you have downloaded the source code) but that’s OK. We ‘ll create all of those
one by one. It is common practice to place any angularJS components related to application’s
layout in a layout folder so go ahead and create this folder. For the side-bar element directive we
used we need two files, one for the directive definition and another for its template. Add the
following two files to the layout folder you created.
spa/layout/sideBar.directive.js
1 (function(app) {
2 'use strict';
3
4 app.directive('sideBar', sideBar);
5
6 function sideBar() {
7 return {
restrict: 'E',
8 replace: true,
9 templateUrl:
10'/scripts/spa/layout/sideBar.html'
11 }
}
12
13
})(angular.module('common.ui'));
14
spa/layout/sidebar.html
<div id="sidebar" class="sidebar-offcanvas">
<div class="col-md-12">
<h3></h3>
1 <ul class="nav nav-pills nav-stacked">
2 <li class="nav-divider"></li>
3 <li class=""><a ng-href="#/">Home<i class="fa fa-home fa-fw pull-
4 right"></i></a></li>
5 <li class="nav-divider"></li>
<li><a ng-href="#/customers/">Customers<i class="fa fa-users fa-fw pull-
6 right"></i></a></li>
7 <li><a ng-href="#/customers/register">Register customer<i class="fa fa-
8 user-plus fa-fw pull-right"></i></a></li>
9 <li class="nav-divider"></li>
<li><a ng-href="#/movies/">Movies<i class="fa fa-film fa-fw pull-
10right"></i></a></li>
11 <li><a ng-href="#/movies/add">Add movie<i class="fa fa-plus-circle fa-fw
12pull-right"></i></a></li>
13 <li class="nav-divider"></li>
14 <li><a ng-href="#/rental/">Rental history<i class="fa fa-leanpub fa-fw
pull-right"></i></a></li>
15 <li class="nav-divider"></li>
16 <li><a ng-href="#/login" ng-if="!userData.isUserLoggedIn">Login<i
17class="fa fa-sign-in fa-fw pull-right"></i></a></li>
18 <li><button type="button" class="btn btn-danger btn-sm" ng-
click="logout();" ng-if="userData.isUserLoggedIn">Logout<i class="fa fa-sign-out fa-fw
19
pull-right"></i></a></li>
20 </ul>
</div>
</div>
This directive will create a collapsible side-bar such as the following when applied.

Let’s see the top-bar directive and its template as well.


spa/layout/topBar.directive.js
1
2 (function(app) {
'use strict';
3
4
app.directive('topBar', topBar);
5
6 function topBar() {
7 return {
8 restrict: 'E',
9 replace: true,
templateUrl: '/scripts/spa/layout/topBar.html'
10 }
11 }
12
13})(angular.module('common.ui'));
14
spa/layout/topBar.html
1 <div class="navbar navbar-default navbar-fixed-top">
<div class="navbar-header">
2 <button type="button" class="navbar-toggle" data-toggle="collapse" data-
3 target=".navbar-collapse">
4 <span class="icon-bar"></span>
5 <span class="icon-bar"></span>
<span class="icon-bar"></span>
6 </button>
7 <a class="navbar-brand active" href="#/">Home Cinema</a>
8 </div>
9 <div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
10 <li><a href="#about">About<i class="fa fa-info-circle fa-fw pull-
11right"></i></a></li>
12 </ul>
13 <ul class="nav navbar-nav navbar-right" ng-if="userData.isUserLoggedIn">
14 <li class="userinfo"><a href="#/">{{username}}<i class="fa fa-user fa-
fw"></i></a></li>
15 </ul>
16 </div>
17</div>
18
You may have noticed I highlighted two lines in the side-bar template’s code. Those lines are
responsible to show or hide the login and log-off buttons respectively depending if the user is
logged in or not. The required functionality will be place at the rootCtrl Controller inside
a home folder.
spa/home/rootCtrl.js
1
2 (function (app) {
3 'use strict';
4
app.controller('rootCtrl', rootCtrl);
5
6
function rootCtrl($scope) {
7
8 $scope.userData = {};
9
10 $scope.userData.displayUserInfo = displayUserInfo;
11 $scope.logout = logout;
12
13
14 function displayUserInfo() {
15
}
16
17 function logout() {
18
19 }
20 }
21
22 })(angular.module('homeCinema'));
23
We will update its contents as soon as we create the membership service. All of our views
require to fetch data from the server and for that a specific apiService will be used through our
application. This service will also be able to display some kind of notifications to the user so let’s
build a notificationService as well. Create a services folder under the spa and add the following
angularJS factory services.
spa/services/notificationService.js
(function (app) {
1 'use strict';
2
3 app.factory('notificationService', notificationService);
4
5 function notificationService() {
6
7 toastr.options = {
"debug": false,
8 "positionClass": "toast-top-right",
9 "onclick": null,
10 "fadeIn": 300,
11 "fadeOut": 1000,
"timeOut": 3000,
12
"extendedTimeOut": 1000
13 };
14
15 var service = {
16 displaySuccess: displaySuccess,
17 displayError: displayError,
displayWarning: displayWarning,
18 displayInfo: displayInfo
19 };
20
21 return service;
22
23 function displaySuccess(message) {
24 toastr.success(message);
}
25
26 function displayError(error) {
27 if (Array.isArray(error)) {
28 error.forEach(function (err) {
29 toastr.error(err);
});
30 } else {
31 toastr.error(error);
32 }
33 }
34
35 function displayWarning(message) {
toastr.warning(message);
36 }
37
38 function displayInfo(message) {
39 toastr.info(message);
40 }
41
}
42
43 })(angular.module('common.core'));
44
45
46
47
48
49
50
51
The notificationService is based on the toastr.js notification library. It displays different type
(style class) of notifications depending on the method invoked, that is success, error, warning
and info.
spa/services/apiService.js
(function (app) {
1 'use strict';
2
3 app.factory('apiService', apiService);
4
5 apiService.$inject = ['$http', '$location', 'notificationService','$rootScope'];
6
7 function apiService($http, $location, notificationService, $rootScope) {
8 var service = {
get: get,
9 post: post
10 };
11
12 function get(url, config, success, failure) {
13 return $http.get(url, config)
.then(function (result) {
14 success(result);
15 }, function (error) {
16 if (error.status == '401') {
17 notificationService.displayError('Authentication required
18 $rootScope.previousState = $location.path();
$location.path('/login');
19 }
20 else if (failure != null) {
21 failure(error);
22 }
});
23
}
24
25 function post(url, data, success, failure) {
26 return $http.post(url, data)
27 .then(function (result) {
28 success(result);
}, function (error) {
29 if (error.status == '401') {
30 notificationService.displayError('Authentication required
31 $rootScope.previousState = $location.path();
32 $location.path('/login');
33 }
else if (failure != null) {
34 failure(error);
35 }
36 });
}
37
38 return service;
39 }
40
41 })(angular.module('common.core'));
42
43
44
45
46
47
48
49
The apiService is quite straight forward. It defines a factory with two basic methods, GET and
POST. Both of these methods can handle 401 errors by redirecting the user at the login view and
saving the previous state so that after a successful login, the user gets back where he/she was.
They also accept a required success callback to invoke and an optional failure one in case of a
failed request. Our spa application is pretty much ready to fetch and post data from the server so
this is the right time to write the first Web API controller.

Web API
We ‘ll try to apply some basic rules for all of Web API Controllers in our application. The first
one is that all of them will inherit from a base class named ApiControllerBase. The basic
responsibility of this class will be handling the Error logging functionality. That’s the only class
where instances of IEntityBaseRepository<Error> will be injected with the centralized Try,
Catch point we talked about at the start of this post. This class of course will inherit from
the ApiController. Create a folder named core inside the Infrastructure and create the base class
for our controllers.
ApiControllerBase.cs
1 public class ApiControllerBase : ApiController
{
2 protected readonly IEntityBaseRepository<Error> _errorsRepository;
3 protected readonly IUnitOfWork _unitOfWork;
4
5 public ApiControllerBase(IEntityBaseRepository<Error> errorsRepository, IUnitO
6 {
_errorsRepository = errorsRepository;
7 _unitOfWork = unitOfWork;
8 }
9
10 protected HttpResponseMessage CreateHttpResponse(HttpRequestMessage request, F
11 {
12 HttpResponseMessage response = null;
13
try
14 {
15 response = function.Invoke();
16 }
17 catch (DbUpdateException ex)
{
18 LogError(ex);
19 response = request.CreateResponse(HttpStatusCode.BadRequest, ex.InnerE
20 }
21 catch (Exception ex)
{
22 LogError(ex);
23 response = request.CreateResponse(HttpStatusCode.InternalServerError,
24 }
25
26 return response;
27 }
28
private void LogError(Exception ex)
29 {
30 try
31 {
32 Error _error = new Error()
{
33 Message = ex.Message,
34 StackTrace = ex.StackTrace,
35 DateCreated = DateTime.Now
36 };
37
38 _errorsRepository.Add(_error);
_unitOfWork.Commit();
39 }
40 catch { }
41 }
42 }
43
44
45
46
47
48
49
50
You will surprised how powerful the CreateHttpResponse function can be when we reach the
discussion section. Notice that this method can handle a DbUpdateException exception as well.
You can omit this type if you want and write more custom methods such as this. Each
controller’s action will start by calling this base method.
If you recall, the home’s page displays the latest movies released plus some genre statistics on
the right. Let’s start from the latest movies. First thing we need to do is create
a ViewModel for Movieentities. For each type of Entity we ‘ll create the respective ViewModel
for the client. All ViewModels will have the relative validation rules based on
the FluentValidation Nuget package. Add the MovieViewModel and GenreViewModel classes
inside the Models folder.
MovieViewModel.cs
1
2
3 [Bind(Exclude = "Image")]
4 public class MovieViewModel : IValidatableObject
{
5 public int ID { get; set; }
6 public string Title { get; set; }
7 public string Description { get; set; }
8 public string Image { get; set; }
9 public string Genre { get; set; }
10 public int GenreId { get; set; }
public string Director { get; set; }
11 public string Writer { get; set; }
12 public string Producer { get; set; }
13 public DateTime ReleaseDate { get; set; }
14 public byte Rating { get; set; }
public string TrailerURI { get; set; }
15 public bool IsAvailable { get; set; }
16 public int NumberOfStocks { get; set; }
17
18 public IEnumerable<ValidationResult> Validate(ValidationContext validationContext
19 {
20 var validator = new MovieViewModelValidator();
var result = validator.Validate(this);
21 return result.Errors.Select(item => new ValidationResult(item.ErrorMessage, ne
22 }
23 }
24
25
We excluded the Image property from MovieViewModel binding cause we will be using a
specific FileUpload action to upload images.
GenreViewModel.cs
1 public class GenreViewModel
2 {
3 public int ID { get; set; }
4 public string Name { get; set; }
5 public int NumberOfMovies { get; set; }
}
6
Create a Validators folder inside the Infrastructure and the MovieViewModelValidator.
MovieViewModelValidator.cs
1 public class MovieViewModelValidator : AbstractValidator<MovieViewModel>
{
2 public MovieViewModelValidator()
3 {
4 RuleFor(movie => movie.GenreId).GreaterThan(0)
5 .WithMessage("Select a Genre");
6
7 RuleFor(movie => movie.Director).NotEmpty().Length(1,100)
.WithMessage("Select a Director");
8
9 RuleFor(movie => movie.Writer).NotEmpty().Length(1,50)
10 .WithMessage("Select a writer");
11
12 RuleFor(movie => movie.Producer).NotEmpty().Length(1, 50)
13 .WithMessage("Select a producer");
14
RuleFor(movie => movie.Description).NotEmpty()
15 .WithMessage("Select a description");
16
17 RuleFor(movie => movie.Rating).InclusiveBetween((byte)0, (byte)5)
18 .WithMessage("Rating must be less than or equal to 5");
19
20 RuleFor(movie => movie.TrailerURI).NotEmpty().Must(ValidTrailerURI)
.WithMessage("Only Youtube Trailers are supported");
21 }
22
23 private bool ValidTrailerURI(string trailerURI)
24 {
25 return (!string.IsNullOrEmpty(trailerURI) && trailerURI.ToLower().StartsW
26 }
}
27
28
29
30
31
Now that we have our first ViewModels and its a validator setup, we can configure
the Automappermappings as well. Add a Mappings folder inside the Infrastructure and create the
following DomainToViewModelMappingProfile Profile class.
1
2
public class DomainToViewModelMappingProfile : Profile
3 {
4 public override string ProfileName
5 {
6 get { return "DomainToViewModelMappings"; }
}
7
8 protected override void Configure()
9 {
10 Mapper.CreateMap<Movie, MovieViewModel>()
11 .ForMember(vm => vm.Genre, map => map.MapFrom(m => m.Genre.Name))
12 .ForMember(vm => vm.GenreId, map => map.MapFrom(m => m.Genre.ID))
.ForMember(vm => vm.IsAvailable, map => map.MapFrom(m => m.Stocks.Any
13 .ForMember(vm => vm.NumberOfStocks, map => map.MapFrom(m => m.Stocks.
14 .ForMember(vm => vm.Image, map => map.MapFrom(m => string.IsNullOrEmp
15
16 Mapper.CreateMap<Genre, GenreViewModel>()
17 .ForMember(vm => vm.NumberOfMovies, map => map.MapFrom(g => g.Movies.
}
18 }
19
20
Notice how we set if a Movie (ViewModel) is available or not by checking if any of its stocks is
available. Add Automapper’s configuration class and make sure to comment out the respective
line in the Bootstrapper class.
Mappings/AutoMapperConfiguration.cs
1
2 public class AutoMapperConfiguration
{
3 public static void Configure()
4 {
5 Mapper.Initialize(x =>
6 {
7 x.AddProfile<DomainToViewModelMappingProfile>();
});
8 }
9 }
10
Bootstrapper.cs
1
public static void Run()
2 {
3 // Configure Autofac
4 AutofacWebapiConfig.Initialize(GlobalConfiguration.Configuration);
5 //Configure AutoMapper
AutoMapperConfiguration.Configure();
6 }
7

We have done so much preparation and now it is time to implement and view all application
requirements in practice. We will start with the Home page.

The Home page


The home page displays information about the latest DVD movie released plus some Genre
statistics. For the first feature will start from creating the required Web API
controller, MoviesController. Add this class inside the Controllers folder.
MoviesController.cs
1 [Authorize(Roles = "Admin")]
[RoutePrefix("api/movies")]
2 public class MoviesController : ApiControllerBase
3 {
4 private readonly IEntityBaseRepository<Movie> _moviesRepository;
5
6 public MoviesController(IEntityBaseRepository<Movie> moviesRepository,
7 IEntityBaseRepository<Error> _errorsRepository, IUnitOfWork _unitOfWork)
: base(_errorsRepository, _unitOfWork)
8 {
9 _moviesRepository = moviesRepository;
10 }
11
12 [AllowAnonymous]
[Route("latest")]
13
public HttpResponseMessage Get(HttpRequestMessage request)
14 {
15 return CreateHttpResponse(request, () =>
16 {
17 HttpResponseMessage response = null;
var movies = _moviesRepository.GetAll().OrderByDescending(m => m.ReleaseD
18
19 IEnumerable<MovieViewModel> moviesVM = Mapper.Map<IEnumerable<Movie>, IEn
20
21 response = request.CreateResponse<IEnumerable<MovieViewModel>>(HttpStatus
22
23 return response;
24 });
}
25 }
26
27
28
29
30
Let’s explain the highlighted lines from top to bottom. All actions for this Controller required the
user not only to be authenticated but also belong to Admin role, except
if AllowAnonymous attribute is applied. All requests to this controller will start with a prefix
of api/movies. The error handling as already explained is handled from the base
class ApiControllerBase and its method CreateHttpResponse. Here we can see for the first time
how this method is actually called. Add the GenresController as well.
GenresController.cs
[Authorize(Roles = "Admin")]
1 [RoutePrefix("api/genres")]
2 public class GenresController : ApiControllerBase
3 {
4 private readonly IEntityBaseRepository<Genre> _genresRepository;
5
public GenresController(IEntityBaseRepository<Genre> genresRepository,
6 IEntityBaseRepository<Error> _errorsRepository, IUnitOfWork _unitOfWork)
7 : base(_errorsRepository, _unitOfWork)
8 {
9 _genresRepository = genresRepository;
10 }
11
[AllowAnonymous]
12 public HttpResponseMessage Get(HttpRequestMessage request)
13 {
14 return CreateHttpResponse(request, () =>
15 {
HttpResponseMessage response = null;
16
var genres = _genresRepository.GetAll().ToList();
17
18 IEnumerable<GenreViewModel> genresVM = Mapper.Map<IEnumerable<Genre>, IEn
19
20 response = request.CreateResponse<IEnumerable<GenreViewModel>>(HttpStatus
21
22 return response;
23 });
}
24 }
25
26
27
28
29
We prepared the server side part, let’s move on to its JavaScript one now. If you recall we ‘ll
follow a structure by feature in our spa, so we will place the two required files for the home page
inside the spa/home folder. We need two files, one template and the respective controller. Let’s
see the template first.
spa/home/i
<hr />
1 <div class="row">
2 <div class="col-md-8">
3 <div class="panel panel-primary" id="panelLatestMovies">
4 <div class="panel-heading">
5 <h3 class="panel-title">Latest Movies Released</h3>
</div>
6
7 <div class="panel-body">
8 <div ng-if="loadingMovies">
9 <div class="col-xs-4"></div>
10 <div class="col-xs-4">
<i class="fa fa-refresh fa-5x fa-spin"></i> <label class="label l
11
</div>
12 <div class="col-xs-4"></div>
13 </div>
14 <div class="col-xs-12 col-sm-6 movie" ng-repeat="movie in latestMovies">
15 <div class="panel panel-default">
<div class="panel-heading">
16 <strong>{{movie.Title}} </strong>
17 </div>
18 <div class="panel-body">
19 <div class="media">
20 <a class="pull-left" href="#">
<a class="fancybox pull-left" rel="gallery1" ng-href="
21 <img class="media-object" height="120" ng-src="../
22 </a>
23
24 </a>
25 <div class="media-body">
<available-movie is-available="{{movie.IsAvailable}}"
26 <div><small>{{movie.Description | limitTo: 70}}...</
27 <label class="label label-info">{{movie.Genre}}</labe
28 </div>
29 <br />
30 </div>
</div>
31 <div class="panel-footer">
32 <span component-rating="{{movie.Rating}}"></span>
33 <a class="fancybox-media pull-right" ng-href="{{movie.Trailer
34 </div>
</div>
35 </div>
36 </div>
37 </div>
38 </div>
<div class="col-md-4">
39 <div class="panel panel-success" id="panelMovieGenres">
40 <div class="panel-heading">
41 <h3 class="panel-title">Movies Genres</h3>
42 </div>
43
<div class="panel-body">
44 <div ng-if="loadingGenres">
45 <div class="col-xs-4"></div>
46 <div class="col-xs-4"><i class="fa fa-refresh fa-5x fa-spin"></i> <la
47 <div class="col-xs-4"></div>
48 </div>
<div id="genres-bar"></div>
49 </div>
50 <div class="panel-footer">
51 <p class="text-center"><em>Wanna add a new Movie? Head over to the add mo
52 <p class="text-center"><a ng-href="#/movies/add" class="btn btn-default">A
</div>
53 </div>
54 </div>
55</div>
56
57
58
59
60
61
62
63
64
65
66
67
I made a convention that the first view rendered for each template will be named index.html. This
means that you will see later the movies/index.html, rental/index.html etc.. We use ng-
if angularJS directive to display a loader (spinner if you prefer) till server side data retrieved
from the server. Let’s see now the controller that binds the data to the template, the indexCtrl.
Add the following file to the home folder as well.
spa/home/indexCtrl.js
(function (app) {
1 'use strict';
2
3 app.controller('indexCtrl', indexCtrl);
4
5 indexCtrl.$inject = ['$scope', 'apiService', 'notificationService'];
6
7 function indexCtrl($scope, apiService, notificationService) {
8 $scope.pageClass = 'page-home';
$scope.loadingMovies = true;
9 $scope.loadingGenres = true;
10 $scope.isReadOnly = true;
11
12 $scope.latestMovies = [];
$scope.loadData = loadData;
13
14 function loadData() {
15 apiService.get('/api/movies/latest', null,
16 moviesLoadCompleted,
17 moviesLoadFailed);
18
apiService.get("/api/genres/", null,
19 genresLoadCompleted,
20 genresLoadFailed);
21 }
22
23 function moviesLoadCompleted(result) {
24 $scope.latestMovies = result.data;
$scope.loadingMovies = false;
25 }
26
27 function genresLoadFailed(response) {
28 notificationService.displayError(response.data);
29 }
30
function moviesLoadFailed(response) {
31 notificationService.displayError(response.data);
32 }
33
34 function genresLoadCompleted(result) {
35 var genres = result.data;
36 Morris.Bar({
element: "genres-bar",
37 data: genres,
38 xkey: "Name",
39 ykeys: ["NumberOfMovies"],
40 labels: ["Number Of Movies"],
41 barRatio: 0.4,
xLabelAngle: 55,
42 hideHover: "auto",
43 resize: 'true'
44 });
45
46 $scope.loadingGenres = false;
}
47
48 loadData();
49 }
50
51 })(angular.module('homeCinema'));
52
53
54
55
56
57
58
59
60
The loadData() function requests movie and genre data from the respective Web API controllers
we previously created. For the movie data only thing needed to do is bind the requested data to
a $scope.latestMovies variable. For the genres data thought, we used genres retrieved data and a
specific div element, genres-bar to create a Morris bar.

Movie Directives
In case you noticed, the index.html template has two custom directives. One to render if the
movie is available and another one to display its rating through the raty.js library. Those two
directives will be used over and over again through our application so let’s take a look at them.
The first one is responsible to render a label element that may be red or green depending if the a
movie is available or not. Since those directives can be used all over the application we ‘ll place
their components inside a directives folder, so go ahead and add this folder under the spa. Create
a availableMovie.html file which will be the template for the new directive.
spa/directives/availableMovie.html
1 <label ng-class="getAvailableClass()">{{getAvailability()}}</label>
Now add the directive definition, availableMovie.directive.js
spa/directives/availableMovie.directive.js
1
(function (app) {
2 'use strict';
3
4 app.directive('availableMovie', availableMovie);
5
6 function availableMovie() {
7 return {
restrict: 'E',
8 templateUrl: "/Scripts/spa/directives/availableMovie.html",
9 link: function ($scope, $element, $attrs) {
10 $scope.getAvailableClass = function () {
11 if ($attrs.isAvailable === 'true')
12 return 'label label-success'
else
13 return 'label label-danger'
14 };
15 $scope.getAvailability = function () {
16 if ($attrs.isAvailable === 'true')
return 'Available!'
17 else
18 return 'Not Available'
19 };
20 }
21 }
}
22
23 })(angular.module('common.ui'));
24
25
26
27

The component-rating which displays a star based rating element, is slightly different in terms of
restriction, since it is used as an element, not as an attribute. I named it component-rating cause
you may want to use it to rate entities other than movies. When you want to render the rating
directive all you have to do is create the following element.
1 <span component-rating="{{movie.Rating}}"></span>
The movie.Rating will hold the rating value. Let’s see the directive’s definition. Place the
following file in the directives folder as well.
componentRating.directive.js
1
2
3 (function(app) {
4 'use strict';
5
6 app.directive('componentRating', componentRating);
7
function componentRating() {
8
return {
9 restrict: 'A',
10 link: function ($scope, $element, $attrs) {
11 $element.raty({
12 score: $attrs.componentRating,
halfShow: false,
13 readOnly: $scope.isReadOnly,
14 noRatedMsg: "Not rated yet!",
15 starHalf: "../Content/images/raty/star-half.png",
16 starOff: "../Content/images/raty/star-off.png",
17 starOn: "../Content/images/raty/star-on.png",
hints: ["Poor", "Average", "Good", "Very Good", "Excellent"],
18 click: function (score, event) {
19 //Set the model value
20 $scope.movie.Rating = score;
21 $scope.$apply();
}
22 });
23 }
24 }
25 }
26
27 })(angular.module('common.ui'));
28
29
One important thing the directive needs to know is if the rating element will be editable or not
and that is configured through the readOnly: $scope.isReadOnly definition. For
the home/index.html we want the rating to be read-only so the controller has the following
declaration:
1 $scope.isReadOnly = true;
Any other controller that requires to edit movie’s rating value will set this value to false.

Account
One of the most important parts in every application is how users are getting authenticated in
order to access authorized resources. We certainly built a custom membership schema and add
an Basic Authentication message handler in Web API, but we haven’t yet created neither the
required Web API AccountController or the relative angularJS component. Let’s start with the
server side first and view the AccountController.
AccountController.cs
1 [Authorize(Roles="Admin")]
[RoutePrefix("api/Account")]
2 public class AccountController : ApiControllerBase
3 {
4 private readonly IMembershipService _membershipService;
5
6 public AccountController(IMembershipService membershipService,
7 IEntityBaseRepository<Error> _errorsRepository, IUnitOfWork _unitOfWork)
: base(_errorsRepository, _unitOfWork)
8 {
9 _membershipService = membershipService;
10 }
11
12 [AllowAnonymous]
[Route("authenticate")]
13 [HttpPost]
14 public HttpResponseMessage Login(HttpRequestMessage request, LoginViewModel user)
15 {
16 return CreateHttpResponse(request, () =>
17 {
HttpResponseMessage response = null;
18
19 if (ModelState.IsValid)
20 {
21 MembershipContext _userContext = _membershipService.ValidateUser(user
22
23 if (_userContext.User != null)
{
24 response = request.CreateResponse(HttpStatusCode.OK, new { succes
25 }
26 else
27 {
28 response = request.CreateResponse(HttpStatusCode.OK, new { succes
}
29 }
30 else
31 response = request.CreateResponse(HttpStatusCode.OK, new { success =
32
33 return response;
34 });
}
35
36 [AllowAnonymous]
37 [Route("register")]
38 [HttpPost]
39 public HttpResponseMessage Register(HttpRequestMessage request, RegistrationViewM
{
40 return CreateHttpResponse(request, () =>
41 {
42 HttpResponseMessage response = null;
43
44 if (!ModelState.IsValid)
{
45 response = request.CreateResponse(HttpStatusCode.BadRequest, new { su
46 }
47 else
48 {
49 Entities.User _user = _membershipService.CreateUser(user.Username, us
50
if (_user != null)
51 {
52 response = request.CreateResponse(HttpStatusCode.OK, new { succes
53 }
54 else
{
55 response = request.CreateResponse(HttpStatusCode.OK, new { succes
56 }
57 }
58
59 return response;
60 });
}
61 }
62
63
64
65
66
67
68
69
70
71
72
73
User send a POST request to api/account/authenticate with their credentials (we ‘ll view
the LoginViewModel soon) and the controller validates the user though the
MemebershipService. If user’s credentials are valid then the returned MembershipContext will
contain the relative user’s User entity. The registration process works pretty much the same. This
time the user posts a request to api/account/register (we ‘ll view
the RegistrationViewModel later) and if the ModelState is valid then the user is created through
the MemebershipService’s CreateUser method. Let’s see now both the LoginViewModel and
the RegistrationViewModel with their respective validators. Add the ViewModel classes in
the Models folder and a AccountViewModelValidators.cs file inside
the Infrastructure/Validators folder to hold both their validators.
LoginViewModel.cs
1
2 public class LoginViewModel : IValidatableObject
3 {
public string Username { get; set; }
4 public string Password { get; set; }
5
6 public IEnumerable<ValidationResult> Validate(ValidationContext validationCon
7 {
8 var validator = new LoginViewModelValidator();
var result = validator.Validate(this);
9 return result.Errors.Select(item => new ValidationResult(item.ErrorMessage
10 }
11 }
12
RegistrationViewModel.cs
1
2 public class RegistrationViewModel : IValidatableObject
3 {
public string Username { get; set; }
4 public string Password { get; set; }
5 public string Email { get; set; }
6
7 public IEnumerable<ValidationResult> Validate(ValidationContext validationCon
8 {
9 var validator = new RegistrationViewModelValidator();
var result = validator.Validate(this);
10 return result.Errors.Select(item => new ValidationResult(item.ErrorMessage
11 }
12 }
13
Infrastructure/Validators/AccountViewModelValidators.cs
public class RegistrationViewModelValidator : AbstractValidator<RegistrationViewModel>
1 {
2 public RegistrationViewModelValidator()
3 {
4 RuleFor(r => r.Email).NotEmpty().EmailAddress()
.WithMessage("Invalid email address");
5
6 RuleFor(r => r.Username).NotEmpty()
7 .WithMessage("Invalid username");
8
9 RuleFor(r => r.Password).NotEmpty()
10 .WithMessage("Invalid password");
11 }
}
12
13 public class LoginViewModelValidator : AbstractValidator<LoginViewModel>
14 {
15 public LoginViewModelValidator()
16 {
RuleFor(r => r.Username).NotEmpty()
17 .WithMessage("Invalid username");
18
19 RuleFor(r => r.Password).NotEmpty()
20 .WithMessage("Invalid password");
}
21 }
22
23
24
25
26
In the Front-End side now, we need to build a MembershipService to handle the following:

MembershipService factory
1. Authenticate user through the Login view

2. Register a user through the Register view

3. Save user’s credentials after successful login or registration in a session cookie


($cookieStore)

4. Remove credentials when user log-off from application

5. Checks if user is logged in or not through the relative $cookieStore repository value
This factory service is mostly depending in the $cookieStore service (ngCookies module) and in
a 3rd party module named ‘$base64’ able to encode – decode strings in base64 format. Logged
in user’s credentials are saved in a $rootScope variable and added as Authorization header in a
each http request. Add the membershipService.js file inside the spa/services folder.
membershipService.js
1 (function (app) {
2 'use strict';
3
app.factory('membershipService', membershipService);
4
5 membershipService.$inject = ['apiService', 'notificationService','$http', '$base6
6
7 function membershipService(apiService, notificationService, $http, $base64, $cook
8
9 var service = {
10 login: login,
register: register,
11 saveCredentials: saveCredentials,
12 removeCredentials: removeCredentials,
13 isUserLoggedIn: isUserLoggedIn
14 }
15
16 function login(user, completed) {
apiService.post('/api/account/authenticate', user,
17 completed,
18 loginFailed);
19 }
20
21 function register(user, completed) {
22 apiService.post('/api/account/register', user,
completed,
23 registrationFailed);
24 }
25
26 function saveCredentials(user) {
27 var membershipData = $base64.encode(user.username + ':' + user.password);
28
$rootScope.repository = {
29 loggedUser: {
30 username: user.username,
31 authdata: membershipData
32 }
33 };
34
$http.defaults.headers.common['Authorization'] = 'Basic ' + membershipDat
35 $cookieStore.put('repository', $rootScope.repository);
36 }
37
38 function removeCredentials() {
39 $rootScope.repository = {};
$cookieStore.remove('repository');
40 $http.defaults.headers.common.Authorization = '';
41 };
42
43 function loginFailed(response) {
44 notificationService.displayError(response.data);
45 }
46
function registrationFailed(response) {
47
48 notificationService.displayError('Registration failed. Try again.');
49 }
50
51 function isUserLoggedIn() {
52 return $rootScope.repository.loggedUser != null;
53 }
54
return service;
55 }
56
57
58
59 })(angular.module('common.core'));
60
61
62
63
64
65
66
67
68
Now that we built this service we are able to handle page refreshes as well. Go ahead and add
the runconfiguration for the main module homeCiname inside the app.js file.
part of app.js
1
2
3
(function () {
4 'use strict';
5
6 angular.module('homeCinema', ['common.core', 'common.ui'])
7 .config(config)
8 .run(run);
9 // routeProvider code ommited
10
run.$inject = ['$rootScope', '$location', '$cookieStore', '$http'];
11
12 function run($rootScope, $location, $cookieStore, $http) {
13 // handle page refreshes
14 $rootScope.repository = $cookieStore.get('repository') || {};
15 if ($rootScope.repository.loggedUser) {
$http.defaults.headers.common['Authorization'] = $rootScope.repository.lo
16
}
17
18 $(document).ready(function () {
19 $(".fancybox").fancybox({
20 openEffect: 'none',
21 closeEffect: 'none'
});
22
23 $('.fancybox-media').fancybox({
24 openEffect: 'none',
25 closeEffect: 'none',
26 helpers: {
27 media: {}
}
28 });
29
30 $('[data-toggle=offcanvas]').click(function () {
31 $('.row-offcanvas').toggleClass('active');
32 });
});
33 }
34
35 })();
36
37
38
I found the opportunity to add same fancy-box related initialization code as well. Now that we
have the membership functionality configured both for the server and the front-end side,
let’s proceed to the login and register views with their controllers. Those angularJS components
will be placed inside an account folder in the spa. Here is the login.html template:
spa/account/login.ht
1
2
<div class="row">
3 <form class="form-signin" role="form" novalidate angular-validator name="userLogin
4 <h3 class="form-signin-heading">Sign in</h3>
5 <div class="form-group">
6 <label for="inputUsername" class="sr-only">Username</label>
7 <input type="text" id="inputUsername" name="inputUsername" class="form-cont
validate-on="blur" required required-message="'Username is require
8 </div>
9 <div class="form-group">
10 <label for="inputPassword" class="sr-only">Password</label>
11 <input type="password" id="inputPassword" name="inputPassword" class="form-
validate-on="blur" required required-message="'Password is require
12 </div>
13 <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button
14 <div class="pull-right">
15 <a ng-href="#/register" class="control-label">Register</a>
16 </div>
</form>
17 </div>
18
19
Here you can see (highlighted lines) for the first time a new library we will be using for
validating form controls, the angularValidator.

And now the loginCtrl controller.


spa/account/loginCtrl.js
1 (function (app) {
2 'use strict';
3
app.controller('loginCtrl', loginCtrl);
4
5 loginCtrl.$inject = ['$scope', 'membershipService', 'notificationService','$rootS
6
7 function loginCtrl($scope, membershipService, notificationService, $rootScope, $l
8 $scope.pageClass = 'page-login';
9 $scope.login = login;
$scope.user = {};
10
11 function login() {
12 membershipService.login($scope.user, loginCompleted)
13 }
14
15 function loginCompleted(result) {
16 if (result.data.success) {
membershipService.saveCredentials($scope.user);
17 notificationService.displaySuccess('Hello ' + $scope.user.username);
18 $scope.userData.displayUserInfo();
19 if ($rootScope.previousState)
20 $location.path($rootScope.previousState);
21 else
$location.path('/');
22 }
23 else {
24 notificationService.displayError('Login failed. Try again.');
25 }
}
26 }
27
28 })(angular.module('common.core'));
29
30
31
32
33
The login function calls the membershipService’s login and passes a success callback. If the
login succeed it does three more things: First, it saves user’s credentials through
membershipService and then displays logged-in user’s info through the rootCtrl controller.
Finally, checks if the user ended in login view cause authentication required to access another
view and if so, redirects him/hem to that view. Let me remind you a small part of the apiService.
part of apiService.js
1 if (error.status == '401') {
2 notificationService.displayError('Authentication required.');
3 $rootScope.previousState = $location.path();
4 $location.path('/login');
}
5

The register.html template and its respective controller work in exactly the same way.

spa/account/register.html
1 <div class="row">
2 <form class="form-signin" role="form" novalidate angular-validator name="userRegis
3 <h3 class="form-signin-heading">Register <label class="label label-danger">Adm
4 <div class="form-group">
<label for="inputUsername" class="">Username</label>
5 <input type="text" name="inputUsername" class="form-control" ng-model="user
6 placeholder="Username" validate-on="blur" required required-message
7 </div>
8 <div class="form-group">
<label for="inputPassword">Password</label>
9 <input type="password" name="inputPassword" class="form-control" ng-model="
10 validate-on="blur" required required-message="'Password is requir
11 </div>
12 <div class="form-group">
13 <label for="inputEmail">Email</label>
<input type="email" name="inputEmail" class="form-control" ng-model="user.e
14 validate-on="blur" required required-message="'Email is required'"
15 </div>
16 <button class="btn btn-lg btn-primary btn-block" type="submit">Register</butto
17 </form>
</div>
18
19
20
21
spa/account/registerCtrl.js
1
2
3 (function (app) {
4 'use strict';
5
app.controller('registerCtrl', registerCtrl);
6
7
registerCtrl.$inject = ['$scope', 'membershipService', 'notificationService', '$r
8
9 function registerCtrl($scope, membershipService, notificationService, $rootScope,
10 $scope.pageClass = 'page-login';
11 $scope.register = register;
12 $scope.user = {};
13
function register() {
14 membershipService.register($scope.user, registerCompleted)
15 }
16
17 function registerCompleted(result) {
18 if (result.data.success) {
19 membershipService.saveCredentials($scope.user);
notificationService.displaySuccess('Hello ' + $scope.user.username);
20 $scope.userData.displayUserInfo();
21 $location.path('/');
22 }
23 else {
notificationService.displayError('Registration failed. Try again.');
24 }
25 }
26 }
27
28 })(angular.module('common.core'));
29
30

Customers
The Customers feature is consisted by 2 views in our SPA application. The first view is
responsible to display all customers. It also supports pagination, filtering current view data and
start a new server search. The second one is the registration view where an employee can register
a new customer. We will start from the server side required components first and then with the
front-end as we did before. Add a new CustomersController Web API controller inside the
controllers folder.
CustomersController.cs
[Authorize(Roles="Admin")]
1 [RoutePrefix("api/customers")]
2 public class CustomersController : ApiControllerBase
3 {
private readonly IEntityBaseRepository<Customer> _customersRepository;
4
5 public CustomersController(IEntityBaseRepository<Customer> customersRepository,
6 IEntityBaseRepository<Error> _errorsRepository, IUnitOfWork _unitOfWork)
7 : base(_errorsRepository, _unitOfWork)
8 {
}
9
10
11
First feature we want to support is the pagination with an optional filter search parameter. For
this to work, the data returned by the Web API action must also include some pagination related
information so that the front-end components can re-build the paginated list. Add the following
generic PaginationSet class inside the Infrastructure/core folder.
PaginationSet.cs
1
2 public class PaginationSet<T>
3 {
4 public int Page { get; set; }
5
public int Count
6 {
7 get
8 {
9 return (null != this.Items) ? this.Items.Count() : 0;
10 }
}
11
12 public int TotalPages { get; set; }
13 public int TotalCount { get; set; }
14
15 public IEnumerable<T> Items { get; set; }
16 }
17
This class holds the list of items we want to render plus all the pagination information we need to
build a paginated list at the front side. Customer entity will have its own ViewModel so let’s
create the CustomerViewModel and its validator. I assume that at this point you know where to
place the following files.
CustomerViewModel.cs
[Bind(Exclude = "UniqueKey")]
1 public class CustomerViewModel : IValidatableObject
2 {
3 public int ID { get; set; }
4 public string FirstName { get; set; }
public string LastName { get; set; }
5 public string Email { get; set; }
6 public string IdentityCard { get; set; }
7 public Guid UniqueKey { get; set; }
8 public DateTime DateOfBirth { get; set; }
public string Mobile { get; set; }
9 public DateTime RegistrationDate { get; set; }
10
11 public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
12 {
var validator = new CustomerViewModelValidator();
13 var result = validator.Validate(this);
14 return result.Errors.Select(item => new ValidationResult(item.ErrorMessage, n
15 }
16 }
17
18
19
20
I have excluded the UniqueKey property from binding since that’s a value to be created on server
side.
CustomerViewModelValidator.cs
1
2
public class CustomerViewModelValidator : AbstractValidator<CustomerViewModel>
3 {
4 public CustomerViewModelValidator()
5 {
6 RuleFor(customer => customer.FirstName).NotEmpty()
.Length(1, 100).WithMessage("First Name must be between 1 - 100 chara
7
8
RuleFor(customer => customer.LastName).NotEmpty()
9 .Length(1, 100).WithMessage("Last Name must be between 1 - 100 charac
10
11 RuleFor(customer => customer.IdentityCard).NotEmpty()
12 .Length(1, 100).WithMessage("Identity Card must be between 1 - 50 cha
13
14 RuleFor(customer => customer.DateOfBirth).NotNull()
.LessThan(DateTime.Now.AddYears(-16))
15 .WithMessage("Customer must be at least 16 years old.");
16
17 RuleFor(customer => customer.Mobile).NotEmpty().Matches(@"^\d{10}$")
18 .Length(10).WithMessage("Mobile phone must have 10 digits");
19
20 RuleFor(customer => customer.Email).NotEmpty().EmailAddress()
21 .WithMessage("Enter a valid Email address");
22
}
23 }
24
25
Don’t forget to add the Automapper mapping from Customer to CustomerViewModel so
switch to DomainToViewModelMappingProfile and add the following line inside
the Configure() function.
part of DomainToViewModelMappingProfile.cs
1 Mapper.CreateMap<Customer, CustomerViewModel>();
Now we can go to CustomersController and create the Search method.
CustomersController Search action
1
2
3 [HttpGet]
4 [Route("search/{page:int=0}/{pageSize=4}/{filter?}")]
5 public HttpResponseMessage Search(HttpRequestMessage request, int? page, int? pageSiz
{
6 int currentPage = page.Value;
7 int currentPageSize = pageSize.Value;
8
9 return CreateHttpResponse(request, () =>
10 {
HttpResponseMessage response = null;
11 List<Customer> customers = null;
12 int totalMovies = new int();
13
14 if (!string.IsNullOrEmpty(filter))
15 {
16 filter = filter.Trim().ToLower();
17
customers = _customersRepository.GetAll()
18 .OrderBy(c => c.ID)
19 .Where(c => c.LastName.ToLower().Contains(filter) ||
20 c.IdentityCard.ToLower().Contains(filter) ||
21 c.FirstName.ToLower().Contains(filter))
.ToList();
22 }
23 else
24 {
25 customers = _customersRepository.GetAll().ToList();
26 }
27
totalMovies = customers.Count();
28 customers = customers.Skip(currentPage * currentPageSize)
29 .Take(currentPageSize)
30 .ToList();
31
32 IEnumerable<CustomerViewModel> customersVM = Mapper.Map<IEnumerable<Customer>
33
PaginationSet<CustomerViewModel> pagedSet = new PaginationSet<CustomerViewMod
34
{
35 Page = currentPage,
36 TotalCount = totalMovies,
37 TotalPages = (int)Math.Ceiling((decimal)totalMovies / currentPageSize),
38 Items = customersVM
};
39
40 response = request.CreateResponse<PaginationSet<CustomerViewModel>>(HttpStatu
41
42 return response;
43 });
44 }
45
46
47
48
49
We will continue with the front-end required angularJS components. As opposed from the
routes we defined in the app.js, we need two files to display the customers view,
the customers.html template and a customersCtrl controller inside a customersCtrl.js file.
part of app.js
1 .when("/customers", {
2 templateUrl: "scripts/spa/customers/customers.html",
3 controller: "customersCtrl"
4 })
Go ahead and add a customers folder inside the spa and create the
following customers.htmltemplate.
spa/customers/c
1 <div class="row">
<div class="panel panel-primary">
2 <div class="panel-heading clearfix">
3 <h4 class="panel-title pull-left" style="padding-top: 7.5px;">Home Cinema Cus
4 <div class="input-group">
5 <input id="inputSearchCustomers" type="search" ng-model="filterCustomers"
6 <div class="input-group-btn">
<button class="btn btn-primary" ng-click="search();"><i class="glyphic
7 <button class="btn btn-primary" ng-click="clearSearch();"><i class="gl
8 </div>
9 </div>
10 </div>
<div class="panel-body">
11 <div class="row">
12 <div class="col-sm-6" ng-repeat="customer in Customers | filter:filterCus
13 <div class="panel panel-default">
14 <div class="panel-heading">
15 <strong>{{customer.FirstName}} {{customer.LastName}}</strong
16
</div>
17 <div class="panel-body">
18 <div class="table-responsive">
19 <table class="table table-condensed shortMargin">
20 <tr>
<td class="shortPadding">Email:</td>
21
<td class="shortPadding"><i>{{customer.Email}}</i
22 </tr>
23 <tr>
24 <td class="shortPadding">Mobile:</td>
25 <td class="shortPadding"><i>{{customer.Mobile}}</
</tr>
26 <tr>
27 <td class="shortPadding">Birth:</td>
28 <td class="shortPadding"><i>{{customer.DateOfBirt
29 </tr>
30 <tr>
<td class="shortPadding">Registered:</td>
31 <td class="shortPadding"><i>{{customer.Registrati
32 </tr>
33 </table>
34 </div>
</div>
35 <div class="panel-footer clearfix">
36 <label class="label label-danger">{{customer.IdentityCard}}</
37 <div class="pull-right">
38 <buton class="btn btn-primary btn-xs" ng-click="openEditD
</div>
39 </div>
40 </div>
41 </div>
42 </div>
43 </div>
<div class="panel-footer">
44 <div class="text-center">
45 <custom-pager page="{{page}}" pages-count="{{pagesCount}}" total-count="{{
46 </div>
47 </div>
</div>
48</div>
49
50
51
52
53
54
55
56
57
58
59
The most important highlighted line is the last one where we build a custom pager element. The
directive we are going to add is responsible to render a paginated list depending on pagination
information retrieved from the server (page, pages-count, total-count). Add the
following pager.htmltemplate and its definition directive inside the layout folder.
spa/layout/pager.html
1
2 <div>
3 <div ng-hide="(!pagesCount || pagesCount < 2)" style="display:inline">
4 <ul class="pagination pagination-sm">
<li><a ng-hide="page == 0" ng-click="search(0)"><<</a></li>
5 <li><a ng-hide="page == 0" ng-click="search(page-1)"><</a></li>
6 <li ng-repeat="n in range()" ng-class="{active: n == page}">
7 <a ng-click="search(n)" ng-if="n != page">{{n+1}}</a>
8 <span ng-if="n == page">{{n+1}}</span>
9 </li>
<li><a ng-hide="page == pagesCount - 1" ng-click="search(pagePlus(1))">></
10 <li><a ng-hide="page == pagesCount - 1" ng-click="search(pagesCount - 1)">
11 </ul>
12 </div>
13 </div>
14
spa/layout/customPager.directive.js
1
2 (function(app) {
3 'use strict';
4
5 app.directive('customPager', customPager);
6
7 function customPager() {
return {
8 scope: {
9 page: '@',
10 pagesCount: '@',
11 totalCount: '@',
searchFunc: '&',
12 customPath: '@'
13 },
14 replace: true,
15 restrict: 'E',
16 templateUrl: '/scripts/spa/layout/pager.html',
controller: ['$scope', function ($scope) {
17 $scope.search = function (i) {
18 if ($scope.searchFunc) {
19 $scope.searchFunc({ page: i });
20 }
};
21
22 $scope.range = function () {
23 if (!$scope.pagesCount) { return []; }
24 var step = 2;
25 var doubleStep = step * 2;
26 var start = Math.max(0, $scope.page - step);
var end = start + 1 + doubleStep;
27 if (end > $scope.pagesCount) { end = $scope.pagesCount; }
28
29 var ret = [];
30 for (var i = start; i != end; ++i) {
31 ret.push(i);
}
32
33
return ret;
34 };
35
36 $scope.pagePlus = function(count)
37 {
38 console.log($scope.page);
return +$scope.page + count;
39 }
40 }]
41 }
42 }
43
44 })(angular.module('homeCinema'));
45
46
47
48
49
50

Now let’s see the customersCtrl controller. This controller is responsible to retrieve data from
Web API and start a new search if the user presses the magnify button next to the textbox. More
over it’s the one that will open a modal popup window when the employee decides to edit a
specific customer. For this popup window we will use the angular-ui $modal service.
spa/customers/customersCtrl.js
1 (function (app) {
2 'use strict';
3
4 app.controller('customersCtrl', customersCtrl);
5
customersCtrl.$inject = ['$scope','$modal', 'apiService', 'notificationService'];
6
7 function customersCtrl($scope, $modal, apiService, notificationService) {
8
9 $scope.pageClass = 'page-customers';
10 $scope.loadingCustomers = true;
11 $scope.page = 0;
12 $scope.pagesCount = 0;
$scope.Customers = [];
13
14 $scope.search = search;
15 $scope.clearSearch = clearSearch;
16
17 $scope.search = search;
18 $scope.clearSearch = clearSearch;
19 $scope.openEditDialog = openEditDialog;
20
21 function search(page) {
page = page || 0;
22
23 $scope.loadingCustomers = true;
24
25 var config = {
26 params: {
27 page: page,
pageSize: 4,
28 filter: $scope.filterCustomers
29 }
30 };
31
32 apiService.get('/api/customers/search/', config,
33 customersLoadCompleted,
customersLoadFailed);
34 }
35
36 function openEditDialog(customer) {
37 $scope.EditedCustomer = customer;
38 $modal.open({
templateUrl: 'scripts/spa/customers/editCustomerModal.html',
39 controller: 'customerEditCtrl',
40 scope: $scope
41 }).result.then(function ($scope) {
42 clearSearch();
43 }, function () {
});
44 }
45
46 function customersLoadCompleted(result) {
47 $scope.Customers = result.data.Items;
48
49 $scope.page = result.data.Page;
50 $scope.pagesCount = result.data.TotalPages;
$scope.totalCount = result.data.TotalCount;
51 $scope.loadingCustomers = false;
52
53 if ($scope.filterCustomers && $scope.filterCustomers.length) {
54 notificationService.displayInfo(result.data.Items.length + ' customer
55 }
56
}
57
58 function customersLoadFailed(response) {
59 notificationService.displayError(response.data);
60 }
61
62 function clearSearch() {
63 $scope.filterCustomers = '';
search();
64 }
65
66 $scope.search();
67 }
68
})(angular.module('homeCinema'));
69
70
71
72
73
74
75
76
77
78
79
Let’s focus on the following part of the customersCtrl controller where the modal window pops
up.
1
2 function openEditDialog(customer) {
$scope.EditedCustomer = customer;
3
$modal.open({
4 templateUrl: 'scripts/spa/customers/editCustomerModal.html',
5 controller: 'customerEditCtrl',
6 scope: $scope
7 }).result.then(function ($scope) {
clearSearch();
8 }, function () {
9 });
10 }
11
When we decide to edit a customer we don’t have to request any data from the server. We have
them already and we can pass them to the customerEditCtrl through the $scope
1 $scope.EditedCustomer = customer;
The popup window isn’t a new view to be rendered but a single pop-up window with a template
and a custom controller. Let’s view both of those components,
the editCustomerModal.html template and the customerEditCtrl controller.
spa/customers/editC
<div class="panel panel-primary">
1 <div class="panel-heading">
2 Edit {{EditedCustomer.FirstName}} {{EditedCustomer.LastName}}
3 </div>
4 <div class="panel-body">
5 <form role="form" novalidate angular-validator name="addCustomerForm" angular-vali
<div class="">
6 <div class="form-group">
7 <div class="row">
8 <div class="col-sm-6">
9 <label class="control-label" for="firstName">First Name</labe
<input type="text" class="form-control" ng-model="EditedCustom
10 validate-on="blur" required required-message="'First N
11 </div>
12
13 <div class="col-sm-6 selectContainer">
14 <label class="control-label" for="lastName">Last Name</label>
<input type="text" class="form-control" ng-model="EditedCustom
15 validate-on="blur" required required-message="'Last Na
16 </div>
17 </div>
18 </div>
<div class="form-group">
19 <div class="row">
20 <div class="col-xs-6">
21 <label class="control-label" for="email">Email Address</label
22 <input type="email" class="form-control" ng-model="EditedCusto
23 validate-on="blur" required required-message="'Email i
</div>
24
25 <div class="col-xs-6 selectContainer">
26 <label class="control-label" for="IdentityCard">Identity Card
27 <input type="text" class="form-control" ng-model="EditedCustom
28 validate-on="blur" required required-message="'Identit
</div>
29 </div>
30 </div>
31 <div class="form-group">
32 <div class="row">
33 <div class="col-xs-6">
<label class="control-label" for="mobilePhone">Mobile</label>
34 <input type="text" ng-model="EditedCustomer.Mobile" class="for
35 validate-on="blur" required required-message="'Mobile
36 </div>
37 <div class="col-xs-6 selectContainer">
38 <label class="control-label" for="dateOfBirth">Date of Birth<
<p class="input-group">
39 <input type="text" class="form-control" name="dateOfBirth"
40open="datepicker.opened"
41 datepicker-options="dateOptions" ng-required="tru
42 validate-on="blur" required required-message="'Dat
<span class="input-group-btn">
43 <button type="button" class="btn btn-default" ng-click
44 </span>
45 </p>
46 </div>
47 </div>
</div>
48 </div>
49 </form>
50 </div>
51 <div class="panel-footer clearfix">
<div class="pull-right">
52 <button type="button" class="btn btn-danger" ng-click="cancelEdit()">Cancel</b
53 <button type="button" class="btn btn-primary" ng-click="updateCustomer()">Upda
54 </div>
55 </div>
56 </div>
57
58
59
60
61
62
63
64
65
66
67
customerEditCtrl.js
1 (function (app) {
'use strict';
2
3 app.controller('customerEditCtrl', customerEditCtrl);
4
5 customerEditCtrl.$inject = ['$scope', '$modalInstance','$timeout', 'apiService', 'no
6
7 function customerEditCtrl($scope, $modalInstance, $timeout, apiService, notificationS
8
9 $scope.cancelEdit = cancelEdit;
$scope.updateCustomer = updateCustomer;
10
11
$scope.openDatePicker = openDatePicker;
12 $scope.dateOptions = {
13 formatYear: 'yy',
14 startingDay: 1
15 };
$scope.datepicker = {};
16
17 function updateCustomer()
18 {
19 console.log($scope.EditedCustomer);
20 apiService.post('/api/customers/update/', $scope.EditedCustomer,
21 updateCustomerCompleted,
updateCustomerLoadFailed);
22 }
23
24 function updateCustomerCompleted(response)
25 {
26 notificationService.displaySuccess($scope.EditedCustomer.FirstName + ' ' + $s
$scope.EditedCustomer = {};
27 $modalInstance.dismiss();
28 }
29
30 function updateCustomerLoadFailed(response)
31 {
32 notificationService.displayError(response.data);
}
33
34 function cancelEdit() {
35 $scope.isEnabled = false;
36 $modalInstance.dismiss();
37 }
38
function openDatePicker($event) {
39 $event.preventDefault();
40 $event.stopPropagation();
41
42 $timeout(function () {
43 $scope.datepicker.opened = true;
});
44
45 $timeout(function () {
46 $('ul[datepicker-popup-wrap]').css('z-index', '10000');
47 }, 100);
48
49 };
50
51 }
52
53})(angular.module('homeCinema'));
54
55
56
57
58
59
60
61
When the update finishes we ensure that we call the $modalInstance.dismiss() function to close
the modal popup window.
You also need to add the Update Web API action method to the CustomersController
CustomersController Update action
1
2 [HttpPost]
3 [Route("update")]
4 public HttpResponseMessage Update(HttpRequestMessage request, CustomerViewModel custo
5 {
6 return CreateHttpResponse(request, () =>
{
7 HttpResponseMessage response = null;
8
9 if (!ModelState.IsValid)
10 {
11 response = request.CreateResponse(HttpStatusCode.BadRequest,
ModelState.Keys.SelectMany(k => ModelState[k].Errors)
12 .Select(m => m.ErrorMessage).ToArray());
13 }
14 else
15 {
16 Customer _customer = _customersRepository.GetSingle(customer.ID);
_customer.UpdateCustomer(customer);
17
18 _unitOfWork.Commit();
19
20 response = request.CreateResponse(HttpStatusCode.OK);
21 }
22
23 return response;
});
24
}
25
26
27
Let’s procceed with the customer’s registration feature by adding the register.html template and
the customersRegCtrl controller.
spa/customers
1 <hr />
<div class="alert alert-info alert-dismissable">
2 <a class="panel-close close" data-dismiss="alert">×</a>
3 <i class="fa fa-user-plus fa-3x"></i>
4 Register <strong>{{movie.Title}}</strong> new customer. Make sure you fill all requi
5 </div>
6 <div class="row">
<form role="form" novalidate angular-validator name="addCustomerForm" angular-validato
7 <div class="col-sm-6">
8 <div class="form-group">
9 <label for="firstName">First Name</label>
10 <div class="form-group">
<input type="text" class="form-control" ng-model="newCustomer.FirstNam
11 validate-on="blur" required required-message="'First Name is r
12
13 </div>
14 </div>
15 <div class="form-group">
16 <label for="lastName">Last Name</label>
<div class="form-group">
17 <input type="text" class="form-control" ng-model="newCustomer.LastName
18 validate-on="blur" required required-message="'Last Name is re
19
20 </div>
21 </div>
<div class="form-group">
22 <label for="email">Email address</label>
23 <div class="form-group">
24 <input type="email" class="form-control" ng-model="newCustomer.Email"
25 validate-on="blur" required required-message="'Email is requir
26 </div>
</div>
27 <div class="form-group">
28 <label for="identityCard">Identity Card</label>
29 <div class="form-group">
30 <input type="text" class="form-control" ng-model="newCustomer.Identity
validate-on="blur" required required-message="'Identity Card i
31
</div>
32 </div>
33 <div class="form-group">
34 <label for="dateOfBirth">Date of Birth</label>
35 <p class="input-group">
<input type="text" class="form-control" name="dateOfBirth" datepicker-
36datepicker-options="dateOptions" ng-required="true" datepicker-append-to-body="true" close
37 validate-on="blur" required required-message="'Date of birth i
38 <span class="input-group-btn">
39 <button type="button" class="btn btn-default" ng-click="openDatePi
40 </span>
</p>
41 </div>
42 <div class="form-group">
43 <label for="mobilePhone">Mobile phone</label>
44 <div class="form-group">
<input type="text" ng-model="newCustomer.Mobile" class="form-control"
45 validate-on="blur" required required-message="'Mobile phone is
46
47 </div>
48 </div>
49 <input type="submit" name="submit" id="submit" value="Submit" class="btn btn-in
</div>
50 </form>
51 <div class="col-sm-5 col-md-push-1">
52 <div class="col-md-12">
53 <div class="alert alert-success">
54 <ul>
<li ng-repeat="message in submission.successMessages track by $index"
55 <strong>{{message}}</strong>
56 </li>
57 </ul>
58 <strong ng-bind="submission.successMessage"><span class="glyphicon glyphi
</div>
59 <div class="alert alert-danger">
60 <ul>
61 <li ng-repeat="error in submission.errorMessages track by $index">
62 <strong>{{error}}</strong>
63 </li>
</ul>
64 </div>
65 </div>
66 </div>
67</div>
68
69
70
71
72
73
74
75
76
77
78
79
80
spa/customers/customersRegCtrl.js
1 (function (app) {
'use strict';
2
3 app.controller('customersRegCtrl', customersRegCtrl);
4
5 customersRegCtrl.$inject = ['$scope', '$location', '$rootScope', 'apiService'];
6
7 function customersRegCtrl($scope, $location, $rootScope, apiService) {
8
9 $scope.newCustomer = {};
10
11 $scope.Register = Register;
12
$scope.openDatePicker = openDatePicker;
13 $scope.dateOptions = {
14 formatYear: 'yy',
15 startingDay: 1
16 };
$scope.datepicker = {};
17
18 $scope.submission = {
19 successMessages: ['Successfull submission will appear here.'],
20 errorMessages: ['Submition errors will appear here.']
21 };
22
23 function Register() {
apiService.post('/api/customers/register', $scope.newCustomer,
24 registerCustomerSucceded,
25 registerCustomerFailed);
26 }
27
28 function registerCustomerSucceded(response) {
$scope.submission.errorMessages = ['Submition errors will appear here.'];
29 console.log(response);
30 var customerRegistered = response.data;
31 $scope.submission.successMessages = [];
32 $scope.submission.successMessages.push($scope.newCustomer.LastName + ' has b
33 $scope.submission.successMessages.push('Check ' + customerRegistered.UniqueKe
$scope.newCustomer = {};
34 }
35
36 function registerCustomerFailed(response) {
37 console.log(response);
38 if (response.status == '400')
39 $scope.submission.errorMessages = response.data;
else
40 $scope.submission.errorMessages = response.statusText;
41 }
42
43 function openDatePicker($event) {
44 $event.preventDefault();
$event.stopPropagation();
45
46 $scope.datepicker.opened = true;
47 };
48 }
49
50})(angular.module('homeCinema'));
51
52
53
54
55
56
57
58
There’s a small problem when rendering the customers registration template. You see there is no
authorized resource to call when this template is rendered but the post action will force the user
to authenticate himself/herself. We would like to avoid this and render the view only if the user
is logged in. What we have used till now (check the customers view), is that when the controller
bound to the view is activated and requests data from the server, if the server requires the user to
be authenticated, then the apiService automatically redirects the user to the login view.
1 if (error.status == '401') {
2 notificationService.displayError('Authentication required.');
3 $rootScope.previousState = $location.path();
4 $location.path('/login');
}
5
On the other hand, in the register customer view the user will be requested to be authenticated
when the employee tries to post the data (new customer) to the server. We can overcome this by
adding a resolve function through the route provider for this route. Switch to the app.js and make
the following modification.
part of app.js
1
2 .when("/customers/register", {
3 templateUrl: "scripts/spa/customers/register.html",
controller: "customersRegCtrl",
4
resolve: { isAuthenticated: isAuthenticated }
5 })
6 // code omitted
7 isAuthenticated.$inject = ['membershipService', '$rootScope','$location'];
8
9 function isAuthenticated(membershipService, $rootScope, $location) {
if (!membershipService.isUserLoggedIn()) {
10 $rootScope.previousState = $location.path();
11 $location.path('/login');
12 }
13 }
14
We use a route resolve function when we want to check a condition before the route actually
changes. In our application we can use it to check if the user is logged in or not and if not
redirect to login view. Now add the Register Web API action to the CustomersController.
CustomersController Register method
public HttpResponseMessage Register(HttpRequestMessage request, CustomerViewModel cus
1 {
2 return CreateHttpResponse(request, () =>
3 {
4 HttpResponseMessage response = null;
5
6 if (!ModelState.IsValid)
{
7 response = request.CreateResponse(HttpStatusCode.BadRequest,
8 ModelState.Keys.SelectMany(k => ModelState[k].Errors)
9 .Select(m => m.ErrorMessage).ToArray());
}
10 else
11 {
12 if (_customersRepository.UserExists(customer.Email, customer.Iden
13 {
ModelState.AddModelError("Invalid user", "Email or Identity C
14 response = request.CreateResponse(HttpStatusCode.BadRequest,
15 ModelState.Keys.SelectMany(k => ModelState[k].Errors)
16 .Select(m => m.ErrorMessage).ToArray());
17 }
18 else
{
19 Customer newCustomer = new Customer();
20 newCustomer.UpdateCustomer(customer);
21 _customersRepository.Add(newCustomer);
22
23 _unitOfWork.Commit();
24
// Update view model
25 customer = Mapper.Map<Customer, CustomerViewModel>(newCustome
26 response = request.CreateResponse<CustomerViewModel>(HttpStat
27 }
28 }
29
30 return response;
});
31 }
32
33
34
35
36
37
38
I have highlighted the line where we update the database customer entity using an extension
method. We have an Automapper map from Customer entity to CustomerViewModel but not
vice-versa. You could do it but I recommend you not to cause it doesn’t work so well
with Entity Framework. That’s why I created an extension method for Customer entities. Add a
new folder named Extensions inside the Infrastructure and create the following class. Then make
sure you include the namespace in the CustomersController class.
EntitiesExtensions.cs
1 public static class EntitiesExtensions
{
2 public static void UpdateCustomer(this Customer customer, CustomerViewModel cus
3 {
4 customer.FirstName = customerVm.FirstName;
5 customer.LastName = customerVm.LastName;
6 customer.IdentityCard = customerVm.IdentityCard;
customer.Mobile = customerVm.Mobile;
7 customer.DateOfBirth = customerVm.DateOfBirth;
8 customer.Email = customerVm.Email;
9 customer.UniqueKey = (customerVm.UniqueKey == null || customerVm.UniqueKe
10 ? Guid.NewGuid() : customerVm.UniqueKey;
customer.RegistrationDate = (customer.RegistrationDate == DateTime.MinVal
11 }
12 }
13
14
15

Movies
The most complex feature in our application is the Movies and that’s because several
requirements are connected to that feature. Let’s recap what we need to do.
1. All movies must be displayed with their relevant information (availability, trailer etc..)
2. Pagination must be used for faster results, and user can either filter the already displayed
movies or search for new ones

3. Clicking on a DVD image must show the movie’s Details view where user can
either edit the movie or rent it to a specific customer if available. This view is accessible
only to authenticated users
4. When employee decides to rent a specific DVD to a customer through the Rent view, it
should be able to search customers through an auto-complete textbox

5. The details view displays inside a panel, rental-history information for this movie, that is
the dates rentals and returnings occurred. From this panel user can search a specific rental
and mark it as returned
6. Authenticated employees should be able to add a new entry to the system. They should be
able to upload a relevant image for the movie as well
We will start with the first two of them that that is display all movies with pagination, filtering
and searching capabilities. We have seen such features when we created the customers base
view. First, let’s add the required Web API action method in the MoviesController.
part of MoviesController.cs
1 [AllowAnonymous]
2 [Route("{page:int=0}/{pageSize=3}/{filter?}")]
3 public HttpResponseMessage Get(HttpRequestMessage request, int? page, int? pageSize,
{
4 int currentPage = page.Value;
5 int currentPageSize = pageSize.Value;
6
7 return CreateHttpResponse(request, () =>
8 {
9 HttpResponseMessage response = null;
List<Movie> movies = null;
10 int totalMovies = new int();
11
12 if (!string.IsNullOrEmpty(filter))
13 {
14 movies = _moviesRepository.GetAll()
15 .OrderBy(m => m.ID)
16 .Where(m => m.Title.ToLower()
.Contains(filter.ToLower().Trim()))
17 .ToList();
18 }
19 else
20 {
movies = _moviesRepository.GetAll().ToList();
21 }
22
23 totalMovies = movies.Count();
24 movies = movies.Skip(currentPage * currentPageSize)
25 .Take(currentPageSize)
26 .ToList();
27
IEnumerable<MovieViewModel> moviesVM = Mapper.Map<IEnumerable<Movie>, IEnumer
28
29 PaginationSet<MovieViewModel> pagedSet = new PaginationSet<MovieViewModel>()
30 {
31 Page = currentPage,
32 TotalCount = totalMovies,
TotalPages = (int)Math.Ceiling((decimal)totalMovies / currentPageSize),
33 Items = moviesVM
34 };
35
36 response = request.CreateResponse<PaginationSet<MovieViewModel>>(HttpStatusCo
37
38 return response;
39 });
}
40
41
42
43
44
45
46
As you can see, this view doesn’t require the user to be authenticated. Once more we used
the PaginationSet class to return additional information for pagination purposes. On the front-end
side, create a movies folder inside the spa, add the movies.html template and
the moviesCtrl.js controller as follow.
spa/movies/m
<div class="row">
1 <div class="panel panel-primary">
2 <div class="panel-heading clearfix">
3 <h4 class="panel-title pull-left" style="padding-top: 7.5px;">Home Cinema Mov
4 <div class="input-group">
<input id="inputSearchMovies" type="search" ng-model="filterMovies" class=
5 <div class="input-group-btn">
6 <button class="btn btn-primary" ng-click="search();"><i class="glyphic
7 <button class="btn btn-primary" ng-click="clearSearch();"><i class="gl
8 </div>
</div>
9 </div>
10
11 <div class="panel-body">
12 <div class="row">
<div class="col-xs-12 col-sm-6 col-md-4" ng-repeat="movie in Movies | fil
13 <div class="media">
14 <a class="pull-left" ng-href="#/movies/{{movie.ID}}" title="View {
15 <img class="media-object" height="120" ng-src="../../Content/i
16 </a>
<div class="media-body">
17 <h4 class="media-heading">{{movie.Title}}</h4>
18 Director: <strong>{{movie.Director}}</strong>
19 <br />
20 Writer: <strong>{{movie.Writer}}</strong>
21 <br />
Producer: <strong>{{movie.Producer}}</strong>
22 <br />
23 <a class="fancybox-media" ng-href="{{movie.TrailerURI}}">Trai
24 </div>
25 <div class="media-bottom">
<span component-rating="{{movie.Rating}}"></span>
26 </div>
27 <label class="label label-info">{{movie.Genre}}</label>
28 <available-movie is-available="{{movie.IsAvailable}}"></available
29 </div>
30 <br /><br />
</div>
31 </div>
32 </div>
33 <div class="panel-footer">
34 <div class="text-center">
35 <custom-pager page="{{page}}" custom-path="{{customPath}}" pages-count="{{
</div>
36 </div>
37 </div>
38</div>
39
40
41
42
43
44
45
46
47
Once again we used both the available-movie and custom-pager directives. Moreover, check
that when we click on an image (lines 18:20) we want to change route and display selected
movie details.
spa/movies/moviesCtrl.js
(function (app) {
1 'use strict';
2
3 app.controller('moviesCtrl', moviesCtrl);
4
5 moviesCtrl.$inject = ['$scope', 'apiService','notificationService'];
6
7 function moviesCtrl($scope, apiService, notificationService) {
$scope.pageClass = 'page-movies';
8 $scope.loadingMovies = true;
9 $scope.page = 0;
10 $scope.pagesCount = 0;
11
12 $scope.Movies = [];
13
$scope.search = search;
14 $scope.clearSearch = clearSearch;
15
16 function search(page) {
17 page = page || 0;
18
19 $scope.loadingMovies = true;
20
21 var config = {
params: {
22 page: page,
23 pageSize: 6,
24 filter: $scope.filterMovies
25 }
26 };
27
apiService.get('/api/movies/', config,
28 moviesLoadCompleted,
29 moviesLoadFailed);
30 }
31
32 function moviesLoadCompleted(result) {
$scope.Movies = result.data.Items;
33 $scope.page = result.data.Page;
34 $scope.pagesCount = result.data.TotalPages;
35 $scope.totalCount = result.data.TotalCount;
36 $scope.loadingMovies = false;
37
38 if ($scope.filterMovies && $scope.filterMovies.length)
{
39 notificationService.displayInfo(result.data.Items.length + ' movies f
40 }
41
42 }
43
44 function moviesLoadFailed(response) {
notificationService.displayError(response.data);
45
}
46
47 function clearSearch() {
48 $scope.filterMovies = '';
49 search();
50 }
51 $scope.search();
52 }
53
})(angular.module('homeCinema'));
54
55
56
57
58
59
60
61
62
63

Let’s continue with the movie details page. Think this page as an control panel for selected
movie where you can edit or rent this movie to a customer and last but not least view all rental
history related to that movie, in other words, who borrowed that movie and its rental
status (borrowed, returned). First, we will prepare the server side part so swith to
the MoviesController and add the following action that returns details for a specific movie.
Check that this action is only available for authenticated users and hence when an employee tries
to display the details view he/she will be forced to log in first.
MoviesController details action
1 [Route("details/{id:int}")]
2 public HttpResponseMessage Get(HttpRequestMessage request, int id)
3 {
return CreateHttpResponse(request, () =>
4 {
5 HttpResponseMessage response = null;
6 var movie = _moviesRepository.GetSingle(id);
7
8 MovieViewModel movieVM = Mapper.Map<Movie, MovieViewModel>(movie);
9
10 response = request.CreateResponse<MovieViewModel>(HttpStatusCode.OK, movieVM)
11
12 return response;
});
13 }
14
15
The movie details page also displays rental-history information so let’s see how to implement
this functionality. What we mean by movie rental-history is all rentals occurred on stock items
related to a specific movie. I remind you that a specific movie may have multiple stock items
(DVDs) and more over, a rental is actually assigned to the stock item, not the movie entity.

Let’s create a new ViewModel named RentalHistoryViewModel to hold the information about a
specific rental. Add the following class in the Models folder.
RentalHistoryViewModel.cs
1
2 public class RentalHistoryViewModel
{
3 public int ID { get; set; }
4 public int StockId { get; set; }
5 public string Customer { get; set; }
6 public string Status { get; set; }
public DateTime RentalDate { get; set; }
7 public Nullable<DateTime> ReturnedDate { get; set; }
8 }
9
The purpose is to return a list of RentalHistoryViewModel items related to the movie being
displayed on the details view. In other words, find all rentals related to stock items that have
foreign key the selected movie’s ID. Add the following Web API RentalsController controller.
RentalsController.cs
1 [Authorize(Roles = "Admin")]
2 [RoutePrefix("api/rentals")]
3 public class RentalsController : ApiControllerBase
{
4 private readonly IEntityBaseRepository<Rental> _rentalsRepository;
5 private readonly IEntityBaseRepository<Customer> _customersRepository;
6 private readonly IEntityBaseRepository<Stock> _stocksRepository;
7 private readonly IEntityBaseRepository<Movie> _moviesRepository;
8
9 public RentalsController(IEntityBaseRepository<Rental> rentalsRepository,
IEntityBaseRepository<Customer> customersRepository, IEntityBaseRepository<Mo
10 IEntityBaseRepository<Stock> stocksRepository,
11 IEntityBaseRepository<Error> _errorsRepository, IUnitOfWork _unitOfWork)
12 : base(_errorsRepository, _unitOfWork)
13 {
_rentalsRepository = rentalsRepository;
14 _moviesRepository = moviesRepository;
15 _customersRepository = customersRepository;
16 _stocksRepository = stocksRepository;
17 }
18 }
19
20
21

We need a private method in this controller which returns the rental-history items as we
previously described.

private method in RentalsController


1
2
3 private List<RentalHistoryViewModel> GetMovieRentalHistory(int movieId)
4 {
5 List<RentalHistoryViewModel> _rentalHistory = new List<RentalHistoryViewModel>();
List<Rental> rentals = new List<Rental>();
6
7 var movie = _moviesRepository.GetSingle(movieId);
8
9 foreach (var stock in movie.Stocks)
10 {
11 rentals.AddRange(stock.Rentals);
}
12
13
foreach (var rental in rentals)
14 {
15 RentalHistoryViewModel _historyItem = new RentalHistoryViewModel()
16 {
17 ID = rental.ID,
StockId = rental.StockId,
18 RentalDate = rental.RentalDate,
19 ReturnedDate = rental.ReturnedDate.HasValue ? rental.ReturnedDate : null,
20 Status = rental.Status,
21 Customer = _customersRepository.GetCustomerFullName(rental.CustomerId)
22 };
23
_rentalHistory.Add(_historyItem);
24 }
25
26 _rentalHistory.Sort((r1, r2) => r2.RentalDate.CompareTo(r1.RentalDate));
27
28 return _rentalHistory;
29 }
30
31
And now we can create the Web API action that the client will invoke when requesting rental
history information.
1 [HttpGet]
2 [Route("{id:int}/rentalhistory")]
public HttpResponseMessage RentalHistory(HttpRequestMessage request, int id)
3 {
4 return CreateHttpResponse(request, () =>
5 {
6 HttpResponseMessage response = null;
7
8 List<RentalHistoryViewModel> _rentalHistory = GetMovieRentalHistory(id);
9
10 response = request.CreateResponse<List<RentalHistoryViewModel>>(HttpStatusCod
11
return response;
12 });
13 }
14
15

In case we wanted to request rental history for movie with ID=4 then the request would be in the
following form:

1 api/rentals/4/rentalhistory
The employee must be able to mark a specific movie rental as Returned when the customer
returns the DVD so let’s add a Return action method as well.
RentalsController return method
1
2
3 [HttpPost]
[Route("return/{rentalId:int}")]
4 public HttpResponseMessage Return(HttpRequestMessage request, int rentalId)
5 {
6 return CreateHttpResponse(request, () =>
7 {
HttpResponseMessage response = null;
8
9
var rental = _rentalsRepository.GetSingle(rentalId);
10
11 if (rental == null)
12 response = request.CreateErrorResponse(HttpStatusCode.NotFound, "Invalid
13 else
14 {
rental.Status = "Returned";
15 rental.Stock.IsAvailable = true;
16 rental.ReturnedDate = DateTime.Now;
17
18 _unitOfWork.Commit();
19
20 response = request.CreateResponse(HttpStatusCode.OK);
21 }
22
return response;
23 });
24 }
25
26
You can mark a movie with ID=4 as Returned with a POST request such as:
1 api/rentals/return/4
At this point we can switch to the front-end and create the details.html template and its relative
controller movieDetailsCtrl. Add the following files inside the movies folder.
spa/movies/
1 <hr />
2 <div class="jumbotron">
3 <div class="container text-center">
4 <img alt="{{movie.Title}}" ng-src="../../../Content/images/movies/{{movie.Image}
<div class="movieDescription"><i><i class="fa fa-quote-left"></i>{{movie.Descrip
5 <br />
6 <div class="btn-group">
7 <button ng-if="movie.IsAvailable" type="button" ng-click="openRentDialog();"
8 <a href="#/movies/edit/{{movie.ID}}" class="btn btn-sm btn-default">Edit mov
</div> <!-- end btn-group -->
9 </div> <!-- end container -->
10 </div>
11
12 <div class="row">
13 <div class="col-md-6">
14 <div class="panel panel-primary">
<div class="panel-heading">
15 <h5>{{movie.Title}}</h5>
16 </div>
17 <div class="panel-body" ng-if="!loadingMovie">
18 <div class="media">
<a class="pull-right" ng-href="#/movies/{{movie.ID}}" title="View {{m
19
<img class="media-object" height="120" ng-src="../../Content/imag
20 </a>
21 <div class="media-body">
22 <h4 class="media-heading">{{movie.Title}}</h4>
23 Directed by: <label>{{movie.Director}}</label><br />
Written by: <label>{{movie.Writer}}</label><br />
24 Produced by: <label>{{movie.Producer}}</label><br />
25 Rating: <span component-rating='{{movie.Rating}}'></span>
26 <br />
27 <label class="label label-info">{{movie.Genre}}</label>
28 <available-movie is-available="{{movie.IsAvailable}}"></availabl
</div>
29 </div>
30 </div>
31 <div class="panel-footer clearfix" ng-if="!loadingMovie">
32 <div class="pull-right">
<a ng-href="{{movie.TrailerURI}}" class="btn btn-primary fancybox-me
33 <a ng-href="#/movies/edit/{{movie.ID}}" class="btn btn-default">Edit
34 </div>
35 </div>
36 <div ng-if="loadingMovie">
37 <div class="col-xs-4"></div>
<div class="col-xs-4">
38 <i class="fa fa-refresh fa-4x fa-spin"></i> <label class="label labe
39 </div>
40 <div class="col-xs-4"></div>
41 </div>
</div>
42
43
44 </div>
45 <div class="col-md-6">
<div class="panel panel-danger shortPanel">
46 <div class="panel-heading clearfix">
47 <h5 class="pull-left">Rentals</h5>
48 <div class="input-group">
49 <input id="inputSearchMovies" type="search" ng-model="filterRentals"
<div class="input-group-btn">
50 <button class="btn btn-primary" ng-click="clearSearch();"><i clas
51 </div>
52 </div>
53 </div>
54 <div class="table-responsive" ng-if="!loadingRentals">
<table class="table table-bordered">
55 <thead>
56 <tr>
57 <th>#</th>
58 <th>Name</th>
<th>Rental date</th>
59 <th>Status</th>
60 <th></th>
61 </tr>
62 </thead>
63 <tbody>
<tr ng-repeat="rental in rentalHistory | filter:filterRentals">
64 <td>{{rental.ID}}</td>
65 <td>{{rental.Customer}}</td>
66 <td>{{rental.RentalDate | date:'fullDate'}}</td>
67 <td ng-class="getStatusColor(rental.Status)">{{rental.Status
68 <td class="text-center">
<button ng-if="isBorrowed(rental)" type="button" class="b
69 </td>
70 </tr>
71 </tbody>
72 </table>
</div>
73 <div ng-if="loadingRentals">
74 <div class="col-xs-4"></div>
75 <div class="col-xs-4">
76 <i class="fa fa-refresh fa-4x fa-spin"></i> <label class="label labe
77 </div>
<div class="col-xs-4"></div>
78 </div>
79 </div>
80 </div>
81 </div>
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
spa/movies/movieDetailsCtrl.cs
1 (function (app) {
'use strict';
2
3 app.controller('movieDetailsCtrl', movieDetailsCtrl);
4
5 movieDetailsCtrl.$inject = ['$scope', '$location', '$routeParams', '$modal', 'apiSe
6
7 function movieDetailsCtrl($scope, $location, $routeParams, $modal, apiService, notif
8 $scope.pageClass = 'page-movies';
$scope.movie = {};
9
$scope.loadingMovie = true;
10 $scope.loadingRentals = true;
11 $scope.isReadOnly = true;
12 $scope.openRentDialog = openRentDialog;
13 $scope.returnMovie = returnMovie;
$scope.rentalHistory = [];
14 $scope.getStatusColor = getStatusColor;
15 $scope.clearSearch = clearSearch;
16 $scope.isBorrowed = isBorrowed;
17
18 function loadMovie() {
19
20 $scope.loadingMovie = true;
21
apiService.get('/api/movies/details/' + $routeParams.id, null,
22 movieLoadCompleted,
23 movieLoadFailed);
24 }
25
26 function loadRentalHistory() {
$scope.loadingRentals = true;
27
28 apiService.get('/api/rentals/' + $routeParams.id + '/rentalhistory', null,
29 rentalHistoryLoadCompleted,
30 rentalHistoryLoadFailed);
31 }
32
33 function loadMovieDetails() {
loadMovie();
34 loadRentalHistory();
35 }
36
37 function returnMovie(rentalID) {
38 apiService.post('/api/rentals/return/' + rentalID, null,
returnMovieSucceeded,
39 returnMovieFailed);
40 }
41
42 function isBorrowed(rental)
43 {
return rental.Status == 'Borrowed';
44 }
45
46 function getStatusColor(status) {
47 if (status == 'Borrowed')
48 return 'red'
49 else {
return 'green';
50 }
51 }
52
53 function clearSearch()
54 {
$scope.filterRentals = '';
55 }
56
57 function movieLoadCompleted(result) {
58 $scope.movie = result.data;
59 $scope.loadingMovie = false;
60 }
61
function movieLoadFailed(response) {
62 notificationService.displayError(response.data);
63 }
64
65 function rentalHistoryLoadCompleted(result) {
66 console.log(result);
67 $scope.rentalHistory = result.data;
$scope.loadingRentals = false;
68 }
69
70 function rentalHistoryLoadFailed(response) {
71 notificationService.displayError(response);
72 }
73
function returnMovieSucceeded(response) {
74 notificationService.displaySuccess('Movie returned to HomeCinema succeesful
75 loadMovieDetails();
76 }
77
78 function returnMovieFailed(response) {
79 notificationService.displayError(response.data);
}
80
81 function openRentDialog() {
82 $modal.open({
templateUrl: 'scripts/spa/rental/rentMovieModal.html',
83 controller: 'rentMovieCtrl',
84 scope: $scope
}).result.then(function ($scope) {
85 loadMovieDetails();
86 }, function () {
87 });
88 }
89
loadMovieDetails();
90 }
91 })(angular.module('homeCinema'));
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

There is one more requirement we need to implement in the details view, the rental. As you may
noticed from the movieDetailsCtrl controller, the rental works with a $modal popup window.
part of movieDetailsCtrl.js
1
2 function openRentDialog() {
$modal.open({
3 templateUrl: 'scripts/spa/rental/rentMovieModal.html',
4 controller: 'rentMovieCtrl',
5 scope: $scope
6 }).result.then(function ($scope) {
7 loadMovieDetails();
}, function () {
8 });
9 }
10
We have seen the $modal popup in action when we were at the edit customer view. Create
the rentMovieModal.html and the rentMovieCtrl controller inside a new folder
named Rental under the spa.
spa/rental/rentM
1 <div class="panel panel-primary">
2 <div class="panel-heading">
3 Rent {{movie.Title}}
4 </div>
<div class="panel-body">
5 <form class="form-horizontal" role="form">
6 <div class="form-group">
7 <div class="col-xs-8 selectContainer">
8 <label class="control-label">Available Stock items</label>
<select ng-model="selectedStockItem" class="form-control black" ng-opt
9 </div>
10 </div>
11 <div class="form-group">
12 <div class="col-xs-8">
13 <label class="control-label">Select customer</label>
<angucomplete-alt id="members"
14 placeholder="Search customers"
15 pause="200"
16 selected-object="selectCustomer"
17 input-changed="selectionChanged"
remote-url="/api/customers?filter="
18 remote-url-data-field=""
19 title-field="FirstName,LastName"
20 description-field="Email"
21 input-class="form-control form-control-small"
22 match-class="red"
text-searching="Searching customers.."
23 text-no-results="No customers found matching your
24 </div>
25 </div>
26 </form>
27 </div>
<div class="panel-footer clearfix">
28 <div class="pull-right">
29 <button type="button" class="btn btn-danger" ng-click="cancelRental()">Cancel<
30 <button type="button" class="btn btn-primary" ng-click="rentMovie()" ng-disabl
31 </div>
</div>
32</div>
33
34
35
36
37
38
39
One new thing to notice in this template is the use of the angucomplete-alt directive. We use it in
order search customers with auto-complete support. In this directive we declared where to
request the data from, the fields to display when an option is selected, a text to display till the
request is completed, what to do when an option is selected or changed, etc.. You can find more
info about this awesome auto-complete directive here.

spa/rental/rentMovieCtrl.js
1 (function (app) {
'use strict';
2
3 app.controller('rentMovieCtrl', rentMovieCtrl);
4
5 rentMovieCtrl.$inject = ['$scope', '$modalInstance', '$location', 'apiService', '
6
7 function rentMovieCtrl($scope, $modalInstance, $location, apiService, notificatio
8
9 $scope.Title = $scope.movie.Title;
$scope.loadStockItems = loadStockItems;
10 $scope.selectCustomer = selectCustomer;
11 $scope.selectionChanged = selectionChanged;
12 $scope.rentMovie = rentMovie;
13 $scope.cancelRental = cancelRental;
14 $scope.stockItems = [];
$scope.selectedCustomer = -1;
15 $scope.isEnabled = false;
16
17 function loadStockItems() {
18 notificationService.displayInfo('Loading available stock items for ' + $s
19
20 apiService.get('/api/stocks/movie/' + $scope.movie.ID, null,
stockItemsLoadCompleted,
21
stockItemsLoadFailed);
22 }
23
24 function stockItemsLoadCompleted(response) {
25 $scope.stockItems = response.data;
26 $scope.selectedStockItem = $scope.stockItems[0].ID;
console.log(response);
27 }
28
29 function stockItemsLoadFailed(response) {
30 console.log(response);
31 notificationService.displayError(response.data);
}
32
33 function rentMovie() {
34 apiService.post('/api/rentals/rent/' + $scope.selectedCustomer + '/' + $sc
35 rentMovieSucceeded,
36 rentMovieFailed);
}
37
38 function rentMovieSucceeded(response) {
39 notificationService.displaySuccess('Rental completed successfully');
40 $modalInstance.close();
41 }
42
43 function rentMovieFailed(response) {
notificationService.displayError(response.data.Message);
44 }
45
46 function cancelRental() {
47 $scope.stockItems = [];
48 $scope.selectedCustomer = -1;
$scope.isEnabled = false;
49 $modalInstance.dismiss();
50 }
51
52 function selectCustomer($item) {
53 if ($item) {
54 $scope.selectedCustomer = $item.originalObject.ID;
$scope.isEnabled = true;
55 }
56 else {
57 $scope.selectedCustomer = -1;
58 $scope.isEnabled = false;
59 }
}
60
61 function selectionChanged($item) {
62 }
63
64 loadStockItems();
65 }
66
})(angular.module('homeCinema'));
67
68
69
70
71
72
73
74
75
76
77
78
When an employee wants to rent a specific movie to a customer, first he must find the stock item
using a code displayed on the DVD the customer requested to borrow. That’s why I highlighted
the above lines in the rentMovieCtrl controller. Moreover, when he finally selects the stock item
and the customer, he needs to press the Rent movie button and send a request to server with
information about the selected customer and stock item as well. With all that said, we need to
implement two more Web API actions. The first one will be in a new Web API Controller
named StocksController and the second one responsible for movie rentals, inside
the RentalsController.
StocksController.cs
1
2
3 [Authorize(Roles="Admin")]
4 [RoutePrefix("api/stocks")]
5 public class StocksController : ApiControllerBase
{
6 private readonly IEntityBaseRepository<Stock> _stocksRepository;
7 public StocksController(IEntityBaseRepository<Stock> stocksRepository,
8 IEntityBaseRepository<Error> _errorsRepository, IUnitOfWork _unitOfWork)
9 : base(_errorsRepository, _unitOfWork)
{
10
_stocksRepository = stocksRepository;
11 }
12
13 [Route("movie/{id:int}")]
14 public HttpResponseMessage Get(HttpRequestMessage request, int id)
15 {
IEnumerable<Stock> stocks = null;
16
17 return CreateHttpResponse(request, () =>
18 {
19 HttpResponseMessage response = null;
20
21 stocks = _stocksRepository.GetAvailableItems(id);
22
23 IEnumerable<StockViewModel> stocksVM = Mapper.Map<IEnumerable<Stock>, IEn
24
response = request.CreateResponse<IEnumerable<StockViewModel>>(HttpStatus
25
26 return response;
27 });
28 }
29 }
30
31
We need to create the StockViewModel class with its validator and of course
the Automappermapping.
Models/StockViewModel.cs
1
2 public class StockViewModel : IValidatableObject
3 {
public int ID { get; set; }
4 public Guid UniqueKey { get; set; }
5 public bool IsAvailable { get; set; }
6
7 public IEnumerable<ValidationResult> Validate(ValidationContext validationCon
8 {
9 var validator = new StockViewModelValidator();
var result = validator.Validate(this);
10 return result.Errors.Select(item => new ValidationResult(item.ErrorMessage
11 }
12 }
13
Infrastructure.Validators/StockViewModelValidator.cs
1
2 public class StockViewModelValidator : AbstractValidator<StockViewModel>
{
3 public StockViewModelValidator()
4 {
5 RuleFor(s => s.ID).GreaterThan(0)
6 .WithMessage("Invalid stock item");
7
8 RuleFor(s => s.UniqueKey).NotEqual(Guid.Empty)
.WithMessage("Invalid stock item");
9 }
10 }
11
part of DomainToViewModelMappingProfile.cs
1 protected override void Configure()
2 {
3 // code omitted
4 Mapper.CreateMap<Stock, StockViewModel>();
}
5
For the rental functionality we need add the following action in the RentalsController
RentalsController Rent action
[HttpPost]
1 [Route("rent/{customerId:int}/{stockId:int}")]
2 public HttpResponseMessage Rent(HttpRequestMessage request, int customerId, int stockI
3 {
4 return CreateHttpResponse(request, () =>
5 {
HttpResponseMessage response = null;
6
7 var customer = _customersRepository.GetSingle(customerId);
8 var stock = _stocksRepository.GetSingle(stockId);
9
10 if (customer == null || stock == null)
11 {
response = request.CreateErrorResponse(HttpStatusCode.NotFound, "Invalid
12 }
13 else
14 {
15 if (stock.IsAvailable)
{
16 Rental _rental = new Rental()
17 {
18 CustomerId = customerId,
19 StockId = stockId,
RentalDate = DateTime.Now,
20 Status = "Borrowed"
21 };
22
23 _rentalsRepository.Add(_rental);
24
25 stock.IsAvailable = false;
26
27 _unitOfWork.Commit();
28
RentalViewModel rentalVm = Mapper.Map<Rental, RentalViewModel>(_renta
29
30 response = request.CreateResponse<RentalViewModel>(HttpStatusCode.Cre
31 }
32 else
33 response = request.CreateErrorResponse(HttpStatusCode.BadRequest, "Se
}
34
35 return response;
36 });
37 }
38
39
40
41
42
43
44
The action accepts the customer id selected from the auto-complete textbox plus the stock’s item
id. Once more we need to add the required view model and Automapper mapping as follow (no
validator this time..).
Models/RentalViewModel.cs
1
public class RentalViewModel
2
{
3 public int ID { get; set; }
4 public int CustomerId { get; set; }
5 public int StockId { get; set; }
6 public DateTime RentalDate { get; set; }
public DateTime ReturnedDate { get; set; }
7 public string Status { get; set; }
8 }
9
part of DomainToViewModelMappingProfile.cs
protected override void Configure()
1 {
2 // code omitted
3 Mapper.CreateMap<Rental, RentalViewModel>();
}
4
5
From the Details view the user has the option to edit the movie by pressing the related button.
This button redirects to route /movies/edit/:id where id is selected movie’s ID. Let’s see the
related route definition in app.js.
part of app.js
1 .when("/movies/edit/:id", {
2 templateUrl: "scripts/spa/movies/edit.html",
3 controller: "movieEditCtrl"
4 })
Here we ‘ll see for the first time how an angularJS controller can capture such a parameter from
the route. Add the edit.html and its controller movieEditCtrl inside the movies folder.
spa/movies
<div id="editMovieWrapper">
1 <hr>
2 <div class="row" ng-if="!loadingMovie">
3 <!-- left column -->
4 <div class="col-xs-3">
<div class="text-center">
5 <img ng-src="../../Content/images/movies/{{movie.Image}}" class="avatar
6 <h6>Change photo...</h6>
7
8 <input type="file" ng-file-select="prepareFiles($files)">
9 </div>
</div>
10
11 <!-- edit form column -->
12 <div class="col-xs-9 personal-info">
13 <div class="alert alert-info alert-dismissable">
14 <a class="panel-close close" data-dismiss="alert">×</a>
15 <i class="fa fa-pencil-square-o"></i>
Edit <strong>{{movie.Title}}</strong> movie. Make sure you fill all req
16 </div>
17 <form class="form-horizontal" role="form" novalidate angular-validator name="
18 <div class="form-group">
19 <div class="row">
<div class="col-xs-8">
20
<label class="control-label">Movie title</label>
21 <input class="form-control" name="title" type="text" ng-model
22 validate-on="blur" required required-message="'Movie
23 </div>
24
25 <div class="col-xs-4 selectContainer">
<label class="control-label">Genre</label>
26 <select ng-model="movie.GenreId" class="form-control black" n
27 <input type="hidden" name="GenreId" ng-value="movie.GenreId"
28 </div>
29 </div>
30 </div>
31
<div class="form-group">
32 <div class="row">
33 <div class="col-xs-4">
<label class="control-label">Director</label>
34 <input class="form-control" type="text" ng-model="movie.Direc
35 validate-on="blur" required required-message="'Movie
36 </div>
37
38 <div class="col-xs-4">
<label class="control-label">Writer</label>
39 <input class="form-control" type="text" ng-model="movie.Write
40 validate-on="blur" required required-message="'Movie
41 </div>
42
43 <div class="col-xs-4">
44 <label class="control-label">Producer</label>
<input class="form-control" type="text" ng-model="movie.Produ
45 validate-on="blur" required required-message="'Movie
46 </div>
47 </div>
48 </div>
49
<div class="form-group">
50 <div class="row">
51 <div class="col-xs-6">
52 <label class="control-label">Release Date</label>
53 <p class="input-group">
54 <input type="text" class="form-control" name="dateRelease
datepicker-options="dateOptions" ng-required="true" datepicker-append-to-body="true" clos
55 validate-on="blur" required required-message="'Da
56 <span class="input-group-btn">
57 <button type="button" class="btn btn-default" ng-clic
58 </span>
59 </p>
</div>
60
61 <div class="col-xs-6">
62 <label class="control-label">Youtube trailer</label>
63 <input class="form-control" type="text" ng-model="movie.Trail
64 validate-on="blur" required required-message="'Movie
invalid-message="'You must enter a valid YouTube URL
65 </div>
66 </div>
67 </div>
68
69 <div class="form-group">
70 <label class="control-label">Description</label>
<textarea class="form-control" ng-model="movie.Description" name="des
71 validate-on="blur" required required-message="'Movie descr
72 </div>
73
74 <div class="form-group col-xs-12">
75 <label class="control-label">Rating</label>
<span component-rating="{{movie.Rating}}" ng-model="movie.Rating" cla
76 </div>
77 <br />
78 <div class="form-group col-xs-4">
79 <label class="control-label"></label>
<div class="">
80 <input type="submit" class="btn btn-primary" value="Update" />
81 <span></span>
82 <a class="btn btn-default" ng-href="#/movies/{{movie.ID}}">Cance
83 </div>
</div>
84 </form>
85 </div>
86 </div>
87 <hr>
88 </div>
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
We used an ng-file-select directive which in conjunction with the 3rd party library angular-file-
upload will handle the movie image uploading through a Web API controller action. You can
read more about file uploading using Web API and angularJS here, where I described the process
step by step.
spa/movies/movieEditCtrl.js
1 (function (app) {
2 'use strict';
3
4 app.controller('movieEditCtrl', movieEditCtrl);
5
movieEditCtrl.$inject = ['$scope', '$location', '$routeParams', 'apiService', 'no
6
7 function movieEditCtrl($scope, $location, $routeParams, apiService, notificationS
8 $scope.pageClass = 'page-movies';
9 $scope.movie = {};
10 $scope.genres = [];
11 $scope.loadingMovie = true;
$scope.isReadOnly = false;
12 $scope.UpdateMovie = UpdateMovie;
13 $scope.prepareFiles = prepareFiles;
14 $scope.openDatePicker = openDatePicker;
15
16 $scope.dateOptions = {
formatYear: 'yy',
17
startingDay: 1
18 };
19 $scope.datepicker = {};
20
21 var movieImage = null;
22
23 function loadMovie() {
24
25 $scope.loadingMovie = true;
26
27 apiService.get('/api/movies/details/' + $routeParams.id, null,
movieLoadCompleted,
28 movieLoadFailed);
29 }
30
31 function movieLoadCompleted(result) {
32 $scope.movie = result.data;
$scope.loadingMovie = false;
33
34 loadGenres();
35 }
36
37 function movieLoadFailed(response) {
38 notificationService.displayError(response.data);
39 }
40
function genresLoadCompleted(response) {
41 $scope.genres = response.data;
42 }
43
44 function genresLoadFailed(response) {
45 notificationService.displayError(response.data);
}
46
47 function loadGenres() {
48 apiService.get('/api/genres/', null,
49 genresLoadCompleted,
50 genresLoadFailed);
51 }
52
function UpdateMovie() {
53 if (movieImage) {
54 fileUploadService.uploadImage(movieImage, $scope.movie.ID, UpdateMovi
55 }
56 else
57 UpdateMovieModel();
}
58
59 function UpdateMovieModel() {
60 apiService.post('/api/movies/update', $scope.movie,
61 updateMovieSucceded,
62 updateMovieFailed);
}
63
64 function prepareFiles($files) {
65 movieImage = $files;
66 }
67
68 function updateMovieSucceded(response) {
69 console.log(response);
notificationService.displaySuccess($scope.movie.Title + ' has been update
70 $scope.movie = response.data;
71 movieImage = null;
}
72
73 function updateMovieFailed(response) {
74 notificationService.displayError(response);
75 }
76
77 function openDatePicker($event) {
$event.preventDefault();
78 $event.stopPropagation();
79
80 $scope.datepicker.opened = true;
81 };
82
83 loadMovie();
84 }
85
})(angular.module('homeCinema'));
86
87
88
89
90
91
92
93
94
95
96
97
98
99
The movieEditController sets a $scope variable named isReadOnly so that the rating component
be editable as we have already discussed. When the user submits the form, if the form is valid it
checks if any selected file exists. If so, starts with the image file uploading and continues with
the movie details updating. If user hasn’t selected an image then only the movie details are being
updated (lines: 60-66). For the image file uploading feature, we injected
an fileUploadService service in our controller. Let’s create that service inside
the services folder.
spa/services/fileUploadService.js
(function (app) {
1 'use strict';
2
3 app.factory('fileUploadService', fileUploadService);
4
5 fileUploadService.$inject = ['$rootScope', '$http', '$timeout', '$upload', 'notif
6
7 function fileUploadService($rootScope, $http, $timeout, $upload, notificationServ
8
$rootScope.upload = [];
9
10 var service = {
11 uploadImage: uploadImage
}
12
13 function uploadImage($files, movieId, callback) {
14 //$files: an array of files selected
15 for (var i = 0; i < $files.length; i++) {
16 var $file = $files[i];
(function (index) {
17 $rootScope.upload[index] = $upload.upload({
18 url: "api/movies/images/upload?movieId=" + movieId, // webapi
19 method: "POST",
20 file: $file
21 }).progress(function (evt) {
}).success(function (data, status, headers, config) {
22 // file is uploaded successfully
23 notificationService.displaySuccess(data.FileName + ' uploaded
24 callback();
25 }).error(function (data, status, headers, config) {
notificationService.displayError(data.Message);
26 });
27 })(i);
28 }
29 }
30
31 return service;
}
32
33 })(angular.module('common.core'));
34
35
36
37
38
39
40
It is time to switch to server side again and support the image file uploading. We need to create
two helper classes. The first one is the class that will contain the file upload result. Add
the FileUploadResult class inside the Infrastructure.Core folder.
FileUploadResult.cs
1 public class FileUploadResult
2 {
3 public string LocalFilePath { get; set; }
4 public string FileName { get; set; }
5 public long FileLength { get; set; }
}
6
We also need to ensure that the request’s content for file uploading is MIME multipart. Let’s
create the following Web API action filter inside the Infrastructure.Core folder.
MimeMultipart.cs
public class MimeMultipart : System.Web.Http.Filters.ActionFilterAttribute
1 {
2 public override void OnActionExecuting(HttpActionContext actionContext)
3 {
if (!actionContext.Request.Content.IsMimeMultipartContent())
4 {
5 throw new HttpResponseException(
6 new HttpResponseMessage(
7 HttpStatusCode.UnsupportedMediaType)
);
8 }
9 }
10
11 public override void OnActionExecuted(HttpActionExecutedContext actionExecuted
12 {
13
14 }
}
15
16
17
18
And here’s the images/upload Web API action in the MoviesController.
MoviesController images/upload action
[MimeMultipart]
1 [Route("images/upload")]
2 public HttpResponseMessage Post(HttpRequestMessage request, int movieId)
3 {
4 return CreateHttpResponse(request, () =>
5 {
HttpResponseMessage response = null;
6
7 var movieOld = _moviesRepository.GetSingle(movieId);
8 if (movieOld == null)
9 response = request.CreateErrorResponse(HttpStatusCode.NotFound, "Invalid
10 else
{
11 var uploadPath = HttpContext.Current.Server.MapPath("~/Content/images/mov
12
13 var multipartFormDataStreamProvider = new UploadMultipartFormProvider(uplo
14
15 // Read the MIME multipart asynchronously
16 Request.Content.ReadAsMultipartAsync(multipartFormDataStreamProvider);
17
18 string _localFileName = multipartFormDataStreamProvider
.FileData.Select(multiPartData => multiPartData.LocalFileName).FirstO
19
20 // Create response
21 FileUploadResult fileUploadResult = new FileUploadResult
22 {
23 LocalFilePath = _localFileName,
24
FileName = Path.GetFileName(_localFileName),
25
26
FileLength = new FileInfo(_localFileName).Length
27 };
28
29 // update database
30 movieOld.Image = fileUploadResult.FileName;
_moviesRepository.Edit(movieOld);
31 _unitOfWork.Commit();
32
33 response = request.CreateResponse(HttpStatusCode.OK, fileUploadResult);
34 }
35
36 return response;
});
37 }
38
39
40
41
42
43
44
For the Update movie operation we need to add an extra extension method as we did with the
Customers. Add the following method inside the EntitiesExtensions class.
1
2 public static void UpdateMovie(this Movie movie, MovieViewModel movieVm)
3 {
movie.Title = movieVm.Title;
4 movie.Description = movieVm.Description;
5 movie.GenreId = movieVm.GenreId;
6 movie.Director = movieVm.Director;
7 movie.Writer = movieVm.Writer;
8 movie.Producer = movieVm.Producer;
movie.Rating = movieVm.Rating;
9 movie.TrailerURI = movieVm.TrailerURI;
10 movie.ReleaseDate = movieVm.ReleaseDate;
11 }
12
Check that the Image property is missing since this property is changed only when uploading a
movie image. Here’s the Update action in the MoviesController.
MoviesController update action
[HttpPost]
1 [Route("update")]
2 public HttpResponseMessage Update(HttpRequestMessage request, MovieViewModel movie)
3 {
4 return CreateHttpResponse(request, () =>
5 {
HttpResponseMessage response = null;
6
7 if (!ModelState.IsValid)
8 {
9 response = request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelSt
10 }
else
11 {
12 var movieDb = _moviesRepository.GetSingle(movie.ID);
13 if (movieDb == null)
14 response = request.CreateErrorResponse(HttpStatusCode.NotFound, "Inva
else
15 {
16 movieDb.UpdateMovie(movie);
17 movie.Image = movieDb.Image;
18 _moviesRepository.Edit(movieDb);
19
_unitOfWork.Commit();
20 response = request.CreateResponse<MovieViewModel>(HttpStatusCode.OK,
21 }
22 }
23
24 return response;
25 });
}
26
27
28
29
30
31

Add movie
The add movie feature is pretty much the same as the edit one. We need one template
named add.html for the add operation and a controller named movieAddCtrl. Add the following
files inside the movies folder.
spa/movies
1 <div id="editMovieWrapper">
<hr>
2
<div class="row">
3 <!-- left column -->
4 <div class="col-xs-3">
5 <div class="text-center">
6 <img ng-src="../../Content/images/movies/unknown.jpg" class="avatar img-
<h6>Add photo...</h6>
7
8 <input type="file" ng-file-select="prepareFiles($files)">
9 </div>
10 </div>
11
12 <!-- edit form column -->
13 <div class="col-xs-9 personal-info">
<div class="alert alert-info alert-dismissable">
14 <a class="panel-close close" data-dismiss="alert">×</a>
15 <i class="fa fa-plus"></i>
16 Add <strong>{{movie.Title}}</strong> movie. Make sure you fill all requ
17 </div>
18
<form class="form-horizontal" role="form" novalidate angular-validator name="
19 <div class="form-group">
20 <div class="row">
21 <div class="col-xs-6 col-sm-4">
22 <label class="control-label">Movie title</label>
23 <input class="form-control" name="title" type="text" ng-model
validate-on="blur" required required-message="'Movie
24 </div>
25
26 <div class="col-xs-6 col-sm-4 selectContainer">
27 <label class="control-label">Genre</label>
28 <select ng-model="movie.GenreId" class="form-control black" n
<input type="hidden" name="GenreId" ng-value="movie.GenreId"
29 </div>
30
31 <div class="col-xs-6 col-sm-4">
32 <label class="control-label">Stocks</label>
33 <div class="input-group number-spinner">
34 <span class="input-group-btn">
<button class="btn btn-default" data-dir="dwn"><span
35 </span>
36 <input type="text" class="form-control text-center" id="i
37 <span class="input-group-btn">
38 <button type="button" class="btn btn-default" id="btn
</span>
39 </div>
40 </div>
41 </div>
42 </div>
43
44 <div class="form-group">
<div class="row">
45 <div class="col-xs-4">
46 <label class="control-label">Director</label>
47 <input class="form-control" type="text" ng-model="movie.Direc
48 validate-on="blur" required required-message="'Movie
49 </div>
50
<div class="col-xs-4">
51 <label class="control-label">Writer</label>
52 <input class="form-control" type="text" ng-model="movie.Write
53 validate-on="blur" required required-message="'Movie
54 </div>
55
<div class="col-xs-4">
56 <label class="control-label">Producer</label>
57 <input class="form-control" type="text" ng-model="movie.Produ
58 validate-on="blur" required required-message="'Movie
59 </div>
60 </div>
</div>
61
62 <div class="form-group">
63 <div class="row">
64 <div class="col-xs-6">
65 <label class="control-label">Release Date</label>
<p class="input-group">
66 <input type="text" class="form-control" name="dateRelease
67 datepicker-options="dateOptions" ng-required="true" datepicker-append-to-body="true" clos
68 validate-on="blur" required required-message="'Da
69 <span class="input-group-btn">
<button type="button" class="btn btn-default" ng-clic
70 </span>
71 </p>
72 </div>
73
74 <div class="col-xs-6">
<label class="control-label">Youtube trailer</label>
75 <input class="form-control" type="text" ng-model="movie.Trail
76 validate-on="blur" required required-message="'Movie
77 invalid-message="'You must enter a valid YouTube URL
78 </div>
79 </div>
</div>
80
81 <div class="form-group">
82 <label class="control-label">Description</label>
83 <textarea class="form-control" ng-model="movie.Description" name="des
84 validate-on="blur" required required-message="'Movie descr
</div>
85
86 <div class="form-group col-xs-12">
87 <label class="control-label">Rating</label>
88 <span component-rating="{{movie.Rating}}" ng-model="movie.Rating" cla
89 </div>
90 <br/>
<div class="form-group col-xs-4">
91 <label class="control-label"></label>
92 <div class="">
93 <input type="submit" class="btn btn-primary" value="Submit movie"
94 <span></span>
95 <a class="btn btn-default" ng-href="#/movies/{{movie.ID}}">Cance
</div>
96 </div>
97 </form>
98 </div>
99 </div>
<hr>
100</div>
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

spa/movies/movieAddCtrl.js
1 (function (app) {
2 'use strict';
3
app.controller('movieAddCtrl', movieAddCtrl);
4
5 movieAddCtrl.$inject = ['$scope', '$location', '$routeParams', 'apiService', 'no
6
7 function movieAddCtrl($scope, $location, $routeParams, apiService, notificationS
8
9 $scope.pageClass = 'page-movies';
10 $scope.movie = { GenreId: 1, Rating: 1, NumberOfStocks: 1 };
11
12 $scope.genres = [];
$scope.isReadOnly = false;
13 $scope.AddMovie = AddMovie;
14 $scope.prepareFiles = prepareFiles;
15 $scope.openDatePicker = openDatePicker;
16 $scope.changeNumberOfStocks = changeNumberOfStocks;
17
$scope.dateOptions = {
18
formatYear: 'yy',
19 startingDay: 1
20 };
21 $scope.datepicker = {};
22
23 var movieImage = null;
24
25 function loadGenres() {
apiService.get('/api/genres/', null,
26 genresLoadCompleted,
27 genresLoadFailed);
28 }
29
30 function genresLoadCompleted(response) {
$scope.genres = response.data;
31 }
32
33 function genresLoadFailed(response) {
34 notificationService.displayError(response.data);
35 }
36
37 function AddMovie() {
AddMovieModel();
38 }
39
40 function AddMovieModel() {
41 apiService.post('/api/movies/add', $scope.movie,
42 addMovieSucceded,
addMovieFailed);
43 }
44
45 function prepareFiles($files) {
46 movieImage = $files;
47 }
48
49 function addMovieSucceded(response) {
notificationService.displaySuccess($scope.movie.Title + ' has been submi
50 $scope.movie = response.data;
51
52 if (movieImage) {
53 fileUploadService.uploadImage(movieImage, $scope.movie.ID, redirectT
54 }
55 else
redirectToEdit();
56 }
57
58 function addMovieFailed(response) {
59 console.log(response);
60 notificationService.displayError(response.statusText);
}
61
62 function openDatePicker($event) {
63 $event.preventDefault();
64 $event.stopPropagation();
65
66 $scope.datepicker.opened = true;
67 };
68 function redirectToEdit() {
69 $location.url('movies/edit/' + $scope.movie.ID);
}
70
71 function changeNumberOfStocks($vent)
72 {
73 var btn = $('#btnSetStocks'),
74 oldValue = $('#inputStocks').val().trim(),
newVal = 0;
75
76 if (btn.attr('data-dir') == 'up') {
77 newVal = parseInt(oldValue) + 1;
78 } else {
79 if (oldValue > 1) {
80 newVal = parseInt(oldValue) - 1;
} else {
81 newVal = 1;
82 }
83 }
84 $('#inputStocks').val(newVal);
$scope.movie.NumberOfStocks = newVal;
85 }
86
87 loadGenres();
88 }
89
90 })(angular.module('homeCinema'));
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
There are two things here to notice. The first one is that we have to setup somehow the number
of stocks for that movie. Normally, each DVD stock item has its own code but just for
convenience, we will create this code automatically in the controller. The other is that since we
need the movie to exist before we upload an image, we ensure that the upload image operation
comes second by uploading the image (if exists) after the movie has been added to database.
When the operation is completed, we redirect to edit the image. Now let’s see the
required Add Web API action in the MoviesController.
MoviesController Add action
1
2
3
4 [HttpPost]
5 [Route("add")]
public HttpResponseMessage Add(HttpRequestMessage request, MovieViewModel movie)
6 {
7 return CreateHttpResponse(request, () =>
8 {
9 HttpResponseMessage response = null;
10
if (!ModelState.IsValid)
11
{
12 response = request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelSt
13 }
14 else
15 {
Movie newMovie = new Movie();
16 newMovie.UpdateMovie(movie);
17
18 for (int i = 0; i < movie.NumberOfStocks; i++)
19 {
20 Stock stock = new Stock()
21 {
IsAvailable = true,
22 Movie = newMovie,
23 UniqueKey = Guid.NewGuid()
24 };
25 newMovie.Stocks.Add(stock);
}
26
27 _moviesRepository.Add(newMovie);
28
29 _unitOfWork.Commit();
30
31 // Update view model
32 movie = Mapper.Map<Movie, MovieViewModel>(newMovie);
33 response = request.CreateResponse<MovieViewModel>(HttpStatusCode.Created,
}
34
35 return response;
36 });
37 }
38
39
40

Rental history
One last thing remained for our Single Page Application is to display a stastistic rental history
for movies that have been rented at least one. We want to create a specific chart for each movie
rented, that shows for each date how many rentals occurred. Let’s start from the server-side. We
need the following ViewModel class that will hold the information required for each movie’s
rental statistics.
Models/TotalRentalHistoryViewModel.cs
1
2
public class TotalRentalHistoryViewModel
3
{
4 public int ID { get; set; }
5 public string Title { get; set; }
6 public string Image { get; set; }
7 public int TotalRentals
{
8 get
9 {
10 return Rentals.Count;
11 }
12 set { }
}
13 public List<RentalHistoryPerDate> Rentals { get; set; }
14 }
15
16 public class RentalHistoryPerDate
17 {
public int TotalRentals { get; set; }
18 public DateTime Date { get; set; }
19 }
20
21
Switch to the RentalsController and add the
following GetMovieRentalHistoryPerDates private method. If you remember, we have already
created a private method GetMovieRentalHistory to get rental history for a specific movie so
getting the rental history for all movies should not be a problem.
RentalsController private method
private List<RentalHistoryPerDate> GetMovieRentalHistoryPerDates(int movieId)
1 {
2 List<RentalHistoryPerDate> listHistory = new List<RentalHistoryPerDate>()
3 List<RentalHistoryViewModel> _rentalHistory = GetMovieRentalHistory(movie
4 if (_rentalHistory.Count > 0)
{
5 List<DateTime> _distinctDates = new List<DateTime>();
6 _distinctDates = _rentalHistory.Select(h => h.RentalDate.Date).Distin
7
8 foreach (var distinctDate in _distinctDates)
9 {
10 var totalDateRentals = _rentalHistory.Count(r => r.RentalDate.Dat
RentalHistoryPerDate _movieRentalHistoryPerDate = new RentalHisto
11 {
12 Date = distinctDate,
13 TotalRentals = totalDateRentals
14 };
15
listHistory.Add(_movieRentalHistoryPerDate);
16 }
17
18 listHistory.Sort((r1, r2) => r1.Date.CompareTo(r2.Date));
19 }
20
return listHistory;
21 }
22
23
24
25
26

Only thing required here was to sort the results by ascending dates. And now the action.

RentalsController TotalRentalHistory action


1
2
3 [HttpGet]
4 [Route("rentalhistory")]
5 public HttpResponseMessage TotalRentalHistory(HttpRequestMessage request)
{
6 return CreateHttpResponse(request, () =>
7 {
8 HttpResponseMessage response = null;
9
10 List<TotalRentalHistoryViewModel> _totalMoviesRentalHistory = new List<TotalR
11
var movies = _moviesRepository.GetAll();
12
13
foreach (var movie in movies)
14 {
15 TotalRentalHistoryViewModel _totalRentalHistory = new TotalRentalHistoryV
16 {
17 ID = movie.ID,
Title = movie.Title,
18 Image = movie.Image,
19 Rentals = GetMovieRentalHistoryPerDates(movie.ID)
20 };
21
22 if (_totalRentalHistory.TotalRentals > 0)
23 _totalMoviesRentalHistory.Add(_totalRentalHistory);
}
24
25 response = request.CreateResponse<List<TotalRentalHistoryViewModel>>(HttpStat
26
27 return response;
28 });
29 }
30
31
On the front-side we need to create a rental.html template and a rentalStatsCtrl controller. Add
the following files inside the rental folder.
spa/rental/rental.html
1
2
3
<hr />
4 <div class="row" ng-repeat="movie in rentals" ng-if="!loadingStatistics">
5 <div class="panel panel-primary">
6 <div class="panel-heading">
7 <span>{{movie.Title}}</span>
8 </div>
<div class="panel-body">
9 <div class="col-xs-3">
10 <div class="panel panel-default">
11 <div class="panel-heading">
12 <strong class="ng-binding">{{movie.Title}} </strong>
13 </div>
<div class="panel-body">
14 <div class="media">
15 <img class="media-object center-block img-responsive cent
16 ng-src="../../Content/images/movies/{{movie.Image}}"
17 </div>
</div>
18 </div>
19 </div>
20 <div class="col-xs-9">
21 <div class="panel panel-default text-center">
22 <div class="panel-body">
<div id="statistics-{{movie.ID}}">
23
24 </div>
25 </div>
26 </div>
27 </div>
</div>
28 </div>
29 </div> <!-- end row -->
30
31
32
spa/rental/rentalStatsCtrl.js
1 (function (app) {
'use strict';
2
3
app.controller('rentStatsCtrl', rentStatsCtrl);
4
5 rentStatsCtrl.$inject = ['$scope', 'apiService', 'notificationService', '$timeout
6
7 function rentStatsCtrl($scope, apiService, notificationService, $timeout) {
8 $scope.loadStatistics = loadStatistics;
9 $scope.rentals = [];
10
function loadStatistics() {
11 $scope.loadingStatistics = true;
12
13 apiService.get('/api/rentals/rentalhistory', null,
14 rentalHistoryLoadCompleted,
15 rentalHistoryLoadFailed);
16 }
17
function rentalHistoryLoadCompleted(result) {
18 $scope.rentals = result.data;
19
20 $timeout(function () {
21 angular.forEach($scope.rentals, function (rental) {
22 if (rental.TotalRentals > 0) {
23
var movieRentals = rental.Rentals;
24
25 Morris.Line({
26 element: 'statistics-' + rental.ID,
27 data: movieRentals,
28 parseTime: false,
29 lineWidth: 4,
xkey: 'Date',
30 xlabels: 'day',
31 resize: 'true',
32 ykeys: ['TotalRentals'],
33 labels: ['Total Rentals']
});
34 }
35 })
36 }, 1000);
37
38 $scope.loadingStatistics = false;
39 }
40
function rentalHistoryLoadFailed(response) {
41 notificationService.displayError(response.data);
42 }
43
44 loadStatistics();
45 }
46
})(angular.module('homeCinema'));
47
48
49
50
51
52
53
54
You need to create the Morris chart after the data have been loaded, otherwise it won’t work.
Notice that we also needed a different id for the element where the chart will be hosted and for
that we used the movie’s id.
At this point, you should be able to run the HomeCinema Single Page Application. In case you
have any problems, you can always download the source code from my Github account and
follow the installation instructions.

Discussion
There are 2 main things I would like to discuss about the application we created. The first one is
talk about scaling the application to support more roles. I’m talking about feature-level scaling
for example suppose that you have an extra requirement that this web application should be
accessible by external users too, the Customers. What would that mean for this application? The
first thing popped up to my mind is more templates, more Web API controllers and last but not
least even more JavaScript files. Guess what, that’s all true. The thing is that after such
requirement our HomeCinemaapplication will not be entirely Single Page Application. It could,
but it would be wrong.
Let me explain what I mean by that. From the server side and the Web API framework, we have
no restrictions at all. We were clever enough to create a custom membership schema with custom
roles and then apply Basic Authentication through message handlers. This means that if you
want to restrict new views only to customers, what you need to do is create a
new Role named Customer and apply an [Authorize(Roles = “Customer”)] attribute to the
respective controllers or actions. The big problem is on the front-end side. You could add more
functionality, templates and controllers upon the same root module homeCinema but believe
me, very soon you wouldn’t be able to maintain all those files. Instead, you can simply break
your application in two Single Page Applications, one responsible for employees and another for
customers. This means that you would have two MVC views, not one (one more action and view
in the HomeController. Each of these views its a Single Page Application by itself and has its
own module. And if you wish, you can go ever further and adapt a new architecture for the front-
end side of your application. Let’s say that both the Admin and Customer roles, require really
many views which means you still have the maintainability issue. Then you have to break your
SPAs even more and have a specific MVC View (spa) for each sub-feature. All the relative sub-
views can share the same Layout page which is responsible to render the
required JavaScript files. Let’s see the architecture graphically.

_Layout pages may have a RenderScript Razor definitions as follow:


1 <!DOCTYPE html>
2 <html lang="en">
<head>
3 <meta charset="utf-8" />
4 <title></title>
5 <meta name="viewport" content="width=device-width" />
6 @Styles.Render("~/Content/css")
7 @Scripts.Render("~/bundles/modernizr")
</head>
8 <body data-ng-app="adminApp">
9 <div class="wrapper">
10 <div class="container">
11 <section>
12 @RenderBody()
</section>
13 </div>
14 </div>
15 </div>
16
17 @Scripts.Render("~/bundles/vendors")
<script src="@Url.Content("~/Scripts/spa/app.js")" type="text/javascript"></s
18
19 @RenderSection("scripts", required: false)
20 <script type="text/javascript">
21 @RenderSection("customScript", required: false)
22 </script>
23 </body>
</html>
24
25
26
27
28
What we did here is render all the required css and vendor files but all MVC views having this
_Layout page can (and must) render their own JavaScript files through
the scripts and customScript sections. The first section concerns angularJS required
components and the latter concerns custom JavaScript code. Remember the following part we
added in the run method of the app.js file?
part of app.js file
1
2 $(document).ready(function () {
3 $(".fancybox").fancybox({
4 openEffect: 'none',
5 closeEffect: 'none'
});
6
7 $('.fancybox-media').fancybox({
8 openEffect: 'none',
9 closeEffect: 'none',
10 helpers: {
11 media: {}
}
12 });
13
14 $('[data-toggle=offcanvas]').click(function () {
15 $('.row-offcanvas').toggleClass('active');
16 });
});
17
18

That’s the type of code that should be written in the customScript section of an MVC view. Now
let’s how an MVC View would look like with this architecture.

1 @section scripts
2 {
3 <script src="@Url.Content("~/Scripts/spa/customController.js")" type="text/javasc
<script src="@Url.Content("~/Scripts/spa/customDirective.js")" type="text/javascr
4 }
5 @section footerScript
6 {
7 angular.bootstrap(document.getElementById("customApp"),['customApp']);
8
$(document).ready(function () {
9 //...
10 });
11 }
12 <div ng-app="customApp" id="customApp">
13 <div ng-view></div>
</div>
14
15
16
One last thing I would like to discuss is the way data repositories are injected into Web
APIcontrollers. We followed a generic repository pattern and all we have to do in order to use
a specific repository is inject it to the constructor. But let’s take a look
the RentalsController constructor.
part of RentalsController
1
2 private readonly IEntityBaseRepository<Rental> _rentalsRepository;
3 private readonly IEntityBaseRepository<Customer> _customersRepository;
4 private readonly IEntityBaseRepository<Stock> _stocksRepository;
private readonly IEntityBaseRepository<Movie> _moviesRepository;
5
6
public RentalsController(IEntityBaseRepository<Rental> rentalsRepository,
7 IEntityBaseRepository<Customer> customersRepository, IEntityBaseRepository<Movie>
8 IEntityBaseRepository<Stock> stocksRepository,
9 IEntityBaseRepository<Error> _errorsRepository, IUnitOfWork _unitOfWork)
10 : base(_errorsRepository, _unitOfWork)
{
11 _rentalsRepository = rentalsRepository;
12 _moviesRepository = moviesRepository;
13 _customersRepository = customersRepository;
14 _stocksRepository = stocksRepository;
15 }
16
It’s kind of a mess, isn’t it? What if a controller requires much more repositories? Well, there’s
another trick you can do to keep your controllers cleaner as much as possible and this is via the
use of a Generic DataRepository Factory. Let’s see how we can accomplish it and re-write
the RentalsController.
First, we need to add a new HttpRequestMessage extension which will allow us to resolve
instances of IEntityBaseRepository<T> so switch to RequestMessageExtensions file and add the
following method.
part of RequestMessageExtensions.c
internal static IEntityBaseRepository<T> GetDataRepository<T>(this HttpRequestMessage r
1 {
2 return request.GetService<IEntityBaseRepository<T>>();
3 }
4
Here we can see how useful the IEntityBase interface is in our application. We can use this
interface to resolve data repositories for our entities. Now let’s create the generic Factory. Add
the following file inside the Infrastructure/Core folder.
DataRepositoryFactory.cs
1
2 public class DataRepositoryFactory : IDataRepositoryFactory
3 {
public IEntityBaseRepository<T> GetDataRepository<T>(HttpRequestMessage reque
4 {
5 return request.GetDataRepository<T>();
6 }
7 }
8
public interface IDataRepositoryFactory
9 {
10 IEntityBaseRepository<T> GetDataRepository<T>(HttpRequestMessage request) whe
11 }
12
This factory has a generic method of type IEntityBase that invokes the extension method we
wrote before. We will use this method in the ApiControllerBase class, which the base class for
our Web API controllers. Before doing this, switch to AutofacWebapiConfig class where we
configured the dependency injection and add the following lines before the end of
the RegisterServices function.
part of AutofacWebapiConfig class
1 // code omitted
2
3 // Generic Data Repository Factory
4 builder.RegisterType<DataRepositoryFactory>()
5 .As<IDataRepositoryFactory>().InstancePerRequest();
6
7 Container = builder.Build();
8
return Container;
9
I don’t want to change the current ApiControllerBase implementation so I will write a new one
just for the demonstration. For start let’s see its definition along with its variables.
Infrastructure/Core/ApiControllerBaseExtended.cs
1 public class ApiControllerBaseExtended : ApiController
{
2 protected List<Type> _requiredRepositories;
3
4 protected readonly IDataRepositoryFactory _dataRepositoryFactory;
5 protected IEntityBaseRepository<Error> _errorsRepository;
6 protected IEntityBaseRepository<Movie> _moviesRepository;
7 protected IEntityBaseRepository<Rental> _rentalsRepository;
protected IEntityBaseRepository<Stock> _stocksRepository;
8 protected IEntityBaseRepository<Customer> _customersRepository;
9 protected IUnitOfWork _unitOfWork;
10
11 private HttpRequestMessage RequestMessage;
12
13 public ApiControllerBaseExtended(IDataRepositoryFactory dataRepositoryFactory
{
14 _dataRepositoryFactory = dataRepositoryFactory;
15 _unitOfWork = unitOfWork;
16 }
17
18 private void LogError(Exception ex)
{
19 try
20 {
21 Error _error = new Error()
22 {
23 Message = ex.Message,
StackTrace = ex.StackTrace,
24 DateCreated = DateTime.Now
25 };
26
27 _errorsRepository.Add(_error);
28 _unitOfWork.Commit();
}
29 catch { }
30 }
31 }
32
33
34
35
36
37
This base class holds references for all types of Data repositories that your application may need.
The most important variable is the _requiredRepositories which eventually will hold the types of
Data repositories a Web API action may require. its constructor has only two dependencies one
of type IDataRepositoryFactory and another of IUnitOfWork. The first one is required to resolve
the data repositories using the new extension method and the other is the one for committing
database changes. Now let’s see the esense of this base class, the
extended CreateHttpResponse method. Add the following methods in to the new base class as
well.
protected HttpResponseMessage CreateHttpResponse(HttpRequestMessage request, List<Typ
1 {
2 HttpResponseMessage response = null;
3
4 try
5 {
RequestMessage = request;
6 InitRepositories(repos);
7 response = function.Invoke();
8 }
9 catch (DbUpdateException ex)
10 {
LogError(ex);
11 response = request.CreateResponse(HttpStatusCode.BadRequest, ex.Inner
12 }
13 catch (Exception ex)
{
14 LogError(ex);
15 response = request.CreateResponse(HttpStatusCode.InternalServerError,
16 }
17
18 return response;
}
19
20 private void InitRepositories(List<Type> entities)
21 {
22 _errorsRepository = _dataRepositoryFactory.GetDataRepository<Error>(Reque
23
24 if (entities.Any(e => e.FullName == typeof(Movie).FullName))
25 {
_moviesRepository = _dataRepositoryFactory.GetDataRepository<Movie>(R
26 }
27
28 if (entities.Any(e => e.FullName == typeof(Rental).FullName))
29 {
30 _rentalsRepository = _dataRepositoryFactory.GetDataRepository<Rental>
31 }
32
if (entities.Any(e => e.FullName == typeof(Customer).FullName))
33 {
34 _customersRepository = _dataRepositoryFactory.GetDataRepository<Custo
35 }
36
37 if (entities.Any(e => e.FullName == typeof(Stock).FullName))
{
38 _stocksRepository = _dataRepositoryFactory.GetDataRepository<Stock>(R
39 }
40
41 if (entities.Any(e => e.FullName == typeof(User).FullName))
42 {
43 _stocksRepository = _dataRepositoryFactory.GetDataRepository<Stock>(R
}
44 }
45
46
47
48
49
50
51
52
53
This method is almost the same as the relative method in the ApiControllerBase class, except
that is accepts an extra parameter of type List<Type>. This list will be used to initialized any
repositories the caller action requires using the private InitRepositories method. Now let’s see
how the RentalsController Web API controller could be written using this new base class. I
created a new RentalsExtendedController class so that you don’t have to change the one you
created before. Let’s the the new definition. Ready?
RentalsExtendedController.cs
1
[Authorize(Roles = "Admin")]
2 [RoutePrefix("api/rentalsextended")]
3 public class RentalsExtendedController : ApiControllerBaseExtended
4 {
5 public RentalsExtendedController(IDataRepositoryFactory dataRepositoryFactory, IUn
: base(dataRepositoryFactory, unitOfWork) { }
6 }
7
Yup, that’s it, no kidding. Now let’s re-write the Rent action that rents a specific movie to a
customer. This method requires 3 data repositories of types Customer, Stock and Rental.
1 [HttpPost]
[Route("rent/{customerId:int}/{stockId:int}")]
2 public HttpResponseMessage Rent(HttpRequestMessage request, int customerId, int stockI
3 {
4 _requiredRepositories = new List<Type>() { typeof(Customer), typeof(Stock), typeo
5
6 return CreateHttpResponse(request, _requiredRepositories, () =>
{
7
HttpResponseMessage response = null;
8
9 var customer = _customersRepository.GetSingle(customerId);
10 var stock = _stocksRepository.GetSingle(stockId);
11
12 if (customer == null || stock == null)
13 {
response = request.CreateErrorResponse(HttpStatusCode.NotFound, "Invalid
14 }
15 else
16 {
17 if (stock.IsAvailable)
18 {
Rental _rental = new Rental()
19 {
20 CustomerId = customerId,
21 StockId = stockId,
22 RentalDate = DateTime.Now,
Status = "Borrowed"
23 };
24
25 _rentalsRepository.Add(_rental);
26
27 stock.IsAvailable = false;
28
29 _unitOfWork.Commit();
30
31 RentalViewModel rentalVm = Mapper.Map<Rental, RentalViewModel>(_renta
32
response = request.CreateResponse<RentalViewModel>(HttpStatusCode.Cre
33 }
34 else
35 response = request.CreateErrorResponse(HttpStatusCode.BadRequest, "Se
36 }
37
return response;
38 });
39 }
40
41
42
43
44
45
46
The only thing an action needs to do is initialize the list of data repositories needs to perform. If
you download the source code of this application from my Github account you will find that I
have re-written the RentalsController and the MoviesController classes with the extended base
class. It’s up to you which of the base classes you prefer to use.
That’s it, we finally finished this post having described step by step how to build a Single Page
Application using Web API and AngularJS. Let me remind you that this post has an e-book
PDF version which you can download for free. You can download the source code for this
project we built herewhere you will also find instructions on how to run
the HomeCinema application. I hope you enjoyed this post or its e-book version as much I did
writing both of them. Please take a moment and post your review at the comments section below.

Anda mungkin juga menyukai