Business Logic should not be contained in controllers. Controllers should be as skinny as possible, ideally follow the patter:
- Find domain entity
- Act on domain entity
- Prepare data for view / return results
Additionally controllers can contain some application logic.
So where do I put my business logic? In Model.
What is Model? Now that's a good question. Please see Microsoft Patterns and Practices article(kudos to AlejandroR for excellent find). In here there are three categories of models:
- View Model: This is simply a data bag, with minimal, if any, logic to pass data from and to views, contains basic field validation.
- Domain Model: Fat model with business logic, operates on a single or multiple data entities (i.e. entity A in a given state than action on entity B)
- Data Model: Storage-aware model, logic contained within a single entity relates only to that entity (i.e. if field a then field b)
Of course, MVC is a paradigm that comes in different varieties. What I describe here is MVC occupying top layer only, vide this article on Wikipedia
Today, MVC and similar model-view-presenter (MVP) are Separation of Concerns design patterns that apply exclusively to the presentation layer of a larger system. In simple scenarios MVC may represent the primary design of a system, reaching directly into the database; however, in most scenarios the Controller and Model in MVC have a loose dependency on either a Service or Data layer/tier. This is all about Client-Server architecture
Server-Side Implementation
Introduction
Creating a well-architected web server application requires meeting the needs of the web client while properly factoring the Microsoft® .NET Framework code on the web server. A web server application is responsible for more than just returning HTML content. Web server applications also involve data models, data access and storage, security, communication, resource management, and internationalization. This chapter covers ways you can integrate technologies in the Microsoft web platform into a coherent architecture that is reliable, testable, and capable of handling demanding web client applications.
The following diagram shows the architecture of the Mileage Stats Reference Implementation (Mileage Stats). In this chapter, the data access and repository layers are discussed first, and then the Model View Controller (MVC) and business services layers. In the context of these layers, we also discuss the distinctions between data models, domain models, and view models. Lastly, you will learn how to provide asynchronous data and validation to web clients.
In this chapter you will learn:
- How to use ADO.NET Entity Framework and the Microsoft SQL Server® Compact Edition to create a data model.
- Techniques to separate concerns among your data model, business logic, and user interface (UI).
- How to support interactive web clients with asynchronous data.
- How to manage data validation at each level of the stack.
The technologies discussed in this chapter are ASP.NET MVC 3, ADO.NET Entity Framework 4, SQL Server Compact Edition 4, and Unity Application Block 2.0.
Creating a Data Access Layer
Data access is a key part of your application. The choice of storage technology and data access patterns can affect the entire application. This section covers an approach that uses rapid modeling techniques and tools while allowing you to migrate to high-scale data storage in the future.
A well-designed data access layer captures the essential structure of the data but omits conditional logic that is specific to the application. When you separate concerns that are specific to data access from those that are specific to the application's logic, the application remains robust and maintainable as you add features over time. The typical concerns of a data access layer include the type of data, the relationships between entities, and constraints.
The data you store is often in a format that is optimized for the storage technology, such as a relational database. Frequently, this format is not convenient for consumption by the application. For example, duration may be stored as a number representing the number of computer clock ticks, but having an instance of a TimeSpan would be easier for the application to use. In this case the data access layer should encapsulate the translation between the storage and in-memory formats. Ideally, the data access layer should not contain any UI or application logic and should fully abstract the underlying storage implementation.
In Mileage Stats, the MileageStats.Model project contains the data model. The data model is part of the data access layer. The structure and strong typing of the classes in this project express the data types, relationships, and constraints inherent in the data. For example, the PricePerUnit property of the FillupEntry class is a double to allow for dollars and cents, the Fillups property of the Vehicle class is an ICollection<FillupEntry> to express a one-to-many relationship, and the DueDate property of the Reminder class is a nullable DateTime to allow it to be optional.
When your application has significant complexity or conditional interaction with the data, you should consider creating a separate domain model that is distinct from your data model. See the "Composing Application Logic" section for guidance about whether or not to create a separate domain model.
Rapid Data Modeling Using the ADO.NET Entity Framework and SQL Server Compact
The ADO.NET Entity Framework provides three ways for you to rapidly create a data model:
- You can use the code-first approach to author standard classes that the ADO.NET Entity Framework uses to generate a database schema.
- You can use the database-first approach, where the ADO.NET Entity Framework generates data model classes from an existing database.
- You can choose to use the model-first approach, where an Entity Data Model (.edmx) can be used to generate the code and database.
The code-first approach is well suited to scenarios like Mileage Stats where developers are defining a new data model that will likely evolve as the application is written and there is no existing database. If you have an existing database, prefer to use stored procedures, or have a data architect on your team, then you may want to use more traditional database modeling techniques that let you generate the data model code from the database.
Using SQL Server Compact with the ADO.NET Entity Framework allows you to use an on-disk database that can easily be recreated whenever your schema changes. It can be seeded with a small dataset for debugging and unit testing. SQL Server Compact has a small footprint and can be migrated to SQL Server Express, SQL Server, or SQL Azure™ when the application is deployed.
Note: |
---|
SQL Server Compact provides query and update functionality, but does not support conditional syntax (such as IF EXISTS), nor does it support stored procedures. Consider other SQL Server editions as your starting point if you need database-centric logic. |
Mileage Stats uses the code-first approach with the ADO.NET Entity Framework and SQL Server Compact. This approach allowed the data model to be built quickly and adapt to changes, and it minimized the day-to-day cost of database setup for the development team.
The ADO.NET Entity Framework lets you seed your database with sample data each time the database is rebuilt. This gives you the opportunity to use realistic sample data while you develop the application. We discovered many issues early in the development process of Mileage Stats because the sample data forced the UI and application logic to work with realistic data.
To use the code-first approach, you first create standard CLR object (POCO) classes. The ADO.NET Entity Framework then infers the database schema from your class structure and your property types. In the following example, the FillupEntry class defines properties that the ADO.NET Entity Framework can map to a database schema.
// Contained in FillupEntry.cs public class FillupEntry { ... public int FillupEntryId { get; set; } public int VehicleId { get; set; } public DateTime Date { get; set; } public int Odometer { get; set; } public double PricePerUnit { get; set; } public double TotalUnits { get; set; } public string Vendor { get; set; } public double TransactionFee { get; set; } public double TotalCost { get { return (this.PricePerUnit*this.TotalUnits) + this.TransactionFee; } } ... }
The ADO.NET Entity Framework maps property types like double, string, and int to their equivalent SQL data type. Fields that represent unique entity identifiers, such as FillupEntryId and VehicleId, are automatically populated. Calculated properties, like TotalCost, that are not saved to the database can be added.
The ADO.NET Entity Framework has three mechanisms for determining the database schema from the class definition:
- Class Inspection. This is inspection of the classes to create a schema. Some of the decisions the ADO.NET Entity Framework makes are based on convention. For example, property names that end in Id are considered unique identifiers. They are auto-populated with database-generated values when records are inserted into the database.
- Attribute Inspection. This involves inspection of data annotation attributes attached to properties. These attributes are found in the System.ComponentModel.DataAnnotations namespace. For example, the KeyAttribute indicates a unique entity identifier. Attributes such as RequiredAttribute and StringLengthAttribute cause the ADO.NET Entity Framework to create column constraints in the database.
- Explicitly using DbModelBuilder. This involves calls to the DbModelBuilder as part of the database creation. These methods directly determine the data types, entity relationships, and constraints for the database schema.
Note: |
---|
Using the data annotation attributes with the ADO.NET Entity Framework affects how the ADO.NET Entity Framework generates the database schema and performs validation of values when the data is saved using DbContext.SaveChanges. However, using the DbModelBuilder only changes the database schema. Which approach you choose can change the error messages you see when invalid data is submitted as well as determine whether a database call is made. |
See the "Further Reading" section for the ADO.NET Entity Framework documentation. It contains the detailed API reference and the steps required to apply each of these techniques.
Mileage Stats used the DbModelBuilder approach to define the storage schema and did not apply any data annotation attributes to the data model classes. This prevented database-specific concerns from being incorporated into the data model and made it possible to change the database schema, if necessary, for other kinds of database deployments. This approach was part of the decision to create a separate data model and domain model. See the "Creating a Business Services Layer" section for more information on this decision.
The domain model in Mileage Stats uses data annotation attributes extensively. See the "Data Validation" section for details on using attributes for validation.
Using the DbModelBuilder to Create a Data Model
In Mileage Stats, the MileageStats.Data.SqlCE project contains the MileageStatsDbContext class. A data model built using the ADO.NET Entity Framework has at least one class derived from DbContext. This class provides the starting point for accessing the data model. It is also used for defining the data model, which results in a database schema.
The MileageStatsDbContext class overrides the OnModelCreating virtual method and uses the DbModelBuilder parameter to provide the ADO.NET Entity Framework with more information about the schema. The task of defining each entity is factored into separate methods that MileageStatsDbContext.OnModelCreating invokes. The following example is one of those methods. It builds the model for the Vehicle class.
// Contained in MileageStatsDbContext.cs private void SetupVehicleEntity(DbModelBuilder modelBuilder) { modelBuilder.Entity<Vehicle>().HasKey(v => v.VehicleId); modelBuilder.Entity<Vehicle>().Property(v => v.VehicleId) .HasDatabaseGeneratedOption( DatabaseGeneratedOption.Identity); modelBuilder.Entity<Vehicle>().Property(v => v.Name) .IsRequired(); modelBuilder.Entity<Vehicle>().Property(v => v.Name) .HasMaxLength(100); modelBuilder.Entity<Vehicle>().Property(v => v.SortOrder); modelBuilder.Entity<Vehicle>().Property(v => v.MakeName) .HasMaxLength(50); modelBuilder.Entity<Vehicle>().Property(v => v.ModelName) .HasMaxLength(50); modelBuilder.Entity<Vehicle>().HasOptional(v => v.Photo); modelBuilder.Entity<Vehicle>().HasMany(v => v.Fillups); modelBuilder.Entity<Vehicle>().HasMany(v => v.Reminders); }
DbModelBuilder allows you to chain calls together because each method returns an object that can be used in subsequent calls. The calls in the preceding example use the Entity<T> method to locate an entity based on the type of the class. The chained Property method locates a property for that entity. Lambda expressions like v => v.VehicleEntryId allow the Property method to work without having to provide the name of the property as a string. The last method call defines the data model type, relationship, or constraint.
It is possible for you to use data annotation attributes in conjunction with calls to DbModelBuilder. Data annotation attributes allow a decentralized approach where relationships and constraints are attached to individual properties on the class. The DbModelBuilder approach gives you centralized control of the data model and a powerful set of modeling options. You should be careful to keep the constraints in sync when mixing approaches. For this reason, it is recommended that you use either data annotation attributes or the DbModelBuilder, and avoid mixing approaches.
Note: |
---|
There is an order of precedence in the ADO.NET Entity Framework when all three mechanisms are used: DbModelBuilder calls override data annotation attributes, which override convention by inspection. |
Creating the Database
Once you define the data model in code, you need to create the database. When you use the code-first approach, the ADO.NET Entity Framework doesn't create the database until the first request for data occurs. You should create the database on application startup rather than on the first request so that the first user isn't forced to wait. Initializing during application startup also reduces the chance of a race condition during database creation.
Note: |
---|
Many web applications built using the ADO.NET Entity Framework contain the auto-generated WebActivatorAttribute code. This attribute automatically calls the database creation and initialization code. Mileage Stats forgoes this approach because the Unity dependency injection container controls the lifetime of the MileageStatsDbContext instance. |
In Global.asax.cs, the Application_Start method initializes the dependency injection container and then initializes the database. The InitializeDatabase method uses the dependency injection container to resolve an instance of the IRepositoryInitializer and then calls the Initialize method. In the following example, the constructor of the RepositoryInitializer configures the database connection and initializer, and the Initialize method requests some data to ensure that the database is created before user requests are processed.
// Contained in RepositoryInitializer.cs public RepositoryInitializer(IUnitOfWork unitOfWork) { ... Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"); Database.SetInitializer( new DropCreateIfModelChangesSqlCeInitializer<MileageStatsDbContext>()); } public void Initialize() { this.Context.Set<Country>().ToList().Count(); ... }
Initializing the Database
The ADO.NET Entity Framework lets you control how your database is created and initialized through the IDatabaseInitializer<T> interface and the Database.SetInitializer method. You can write the initializer with the logic you need to create and populate your database.
In Mileage Stats, the MileageStats.Data.SqlCe project contains three classes that can initialize the database: CreateIfNotExistsSqlCeInitializer, DropCreateAlwaysSqlCeInitializer, and DropCreateIfModelChangesSqlCeInitializer. All three inherit from the SqlCeInitializer base class that implements the IDatabaseInitializer<T> interface.
Note: |
---|
When you use NuGet to add the ADO.NET Entity Framework to your project, the package manager will generate some default initializers similar to those found in Mileage Stats. Mileage Stats classes are modified versions of the original generated classes. The modifications allow each initializer to share the database seeding code used to start the application with sample data. |
Each class implements a different strategy for creating the database. Mileage Stats defaults to the DropCreateIfModelChangesSqlCeInitializer to drop and create the database anytime the data model changes the schema. This can be very useful during product development when the data model is evolving and the database doesn't contain real data.
Depending on how your database is deployed, you may need to change the default initializer or use another mechanism for initializing the database. If you deploy a new version of the application where the schema needs to be upgraded, you would either need to write an initializer that upgrades the database, or run upgrade scripts before deployment of the newer version. Otherwise, you would lose all the data stored in the database.
Optimized Data Access
Many application data models are hierarchical, with one-to-many and many-to-many relationships between entities. On the other hand, web applications are connectionless and stateless; they take a request and produce a response. You should avoid loading large model hierarchies for requests that only need a subset of the data. Loading more data than is necessary places additional processor, memory, and bandwidth pressure on the server and can limit scalability and performance.
Fortunately, the ADO.NET Entity Framework provides powerful querying support in the DbSet class that allows you to return just the data you need. In Mileage Stats, the VehicleRepository class uses the Where and Include methods on the DbSet to control the data retrieved, as shown in the following code.
// Contained in VehicleRepository.cs public Vehicle GetVehicle(int userId, int vehicleId) { return this.GetDbSet<Vehicle>() .Include("Fillups") .Include("Reminders") .Where(v => v.VehicleId == vehicleId && v.UserId == userId) .Single(); }
The ADO.NET Entity Framework does not automatically populate ICollection<T> properties like Vehicles, Fill-ups, and Reminder, but does lazy load them when accessed. When lazy loading is turned off (as it is in Mileage Stats,) these properties must be included explicitly. Properties marked optional by data annotation attributes or DbModelBuilder calls, such as theFillupEntry.Distance property, must also be included explicitly to be retrieved. The previous code sample is an example of calling Include explicitly.
The ADO.NET Entity Framework has features that support lazy-loaded properties and change tracking. When using the code-first approach, lazy loading is done by applying the virtual keyword, and change tracking is done through having standard get and set methods along with using ICollection<T> for one-to-many relationships. The ADO.NET Entity Framework also supports these features by implementing the IEntityWithChangeTracker or IEntityWithRelationships interfaces.
Implementing the Repository Pattern
The Repository pattern assists in separating data storage concerns from the application logic. This pattern is especially beneficial when you use the ADO.NET Entity Framework because it allows you to hide ADO.NET Entity Framework-specific classes such as DbContext and DbSet, to optimize the shape of the data returned to the application, to coordinate updates, and to unit test your application without requiring access to physical data storage. See the "Further Reading" section for a formal definition of the repository pattern.
A repository is a set of interfaces and implementations providing methods for data access. The interfaces do not expose any types specific to data storage. You can choose how many repositories to create based on how granularly you want to control data access within your application.
In Mileage Stats, the MileageStats.Data project contains the repository interfaces and the MileageStats.Data.SqlCe project contains the repository implementations. The Mileage Stats repositories map closely to the data entities to match the usage pattern from the business services layer. The following code shows the IReminderRepository interface.
// Contained in IReminderRepository.cs public interface IReminderRepository { void Create(int vehicleId, Reminder reminder); Reminder GetReminder(int reminderId); void Update(Reminder reminder); void Delete(int reminderId); IEnumerable<Reminder> GetRemindersForVehicle(int vehicleId); IEnumerable<Reminder> GetOverdueReminders(int vehicleId, DateTime forDate, int forOdometer); IEnumerable<Reminder> GetUpcomingReminders(int vehicleId, DateTime forStartDate, DateTime forEndDate, int odometer, int warningOdometer); }
Note: |
---|
The IReminderRepository interface returns collections as IEnumerable<T>, rather than IList<T> or ICollection<T>. This was an intentional design choice to prevent the direct addition of entities to the collections. To create a new reminder, the developer must use the Create method. In Mileage Stats, the implementation of the IReminderRepository calls ToList before returning the IEnumerable<T>. This is to ensure that the query is executed inside the repository. If ToList was not called, then the repository would return an IQueryable<T> and the database would not be accessed until something iterated over the IQueryable<T> object. The problem with returning an IQueryable<T> is that a developer consuming the API is likely to assume that the query has already executed and that you are working with the results. If you iterate over the query more than once, it will result in multiple calls to the database. If you specifically want your repository to return queries instead of results, use the IQueryable<T> on the interface in order to make your intention explicit. |
Because web applications and services follow a request/response pattern, incoming data is built from the POST form data. This means that the incoming object class was not retrieved from the DbContext and cannot be updated because it is not attached to the context. Using the repository pattern with the ADO.NET Entity Framework creates the proper place to deal with attached and detached entities and set entity state.
In the following example, the Update method in the VehicleRepository is passed an entity that is not attached to the ADO.NET Entity Framework context. The Update method locates the corresponding attached entity, updates the attached entity, and ensures the attached entity state is set correctly.
// Contained in VehicleRepository.cs public void Update(Vehicle updatedVehicle) { Vehicle vehicleToUpdate = this.GetDbSet<Vehicle>().Where(v => v.VehicleId == updatedVehicle.VehicleId).First(); vehicleToUpdate.Name = updatedVehicle.Name; vehicleToUpdate.Year = updatedVehicle.Year; vehicleToUpdate.MakeName = updatedVehicle.MakeName; vehicleToUpdate.ModelName = updatedVehicle.ModelName; vehicleToUpdate.SortOrder = updatedVehicle.SortOrder; vehicleToUpdate.PhotoId = updatedVehicle.PhotoId; this.SetEntityState(vehicleToUpdate, vehicleToUpdate.VehicleId == 0 ? EntityState.Added : EntityState.Modified); this.UnitOfWork.SaveChanges(); }
Composing Application Logic
Web client applications built for rich user interactivity are often more complex than those built for clients that post back synchronously on each mouse click and always display static HTML in response. Web applications that provide interactive behavior on a single page (via Ajax method calls, JavaScript templates, and secondary data requests) require careful composition of server application code. This section covers several techniques and considerations to help you create maintainable applications that provide a rich set of services to interactive clients.
Factoring Application Code with ASP.NET MVC
Because ASP.NET MVC is a web platform technology built around a design pattern, following the MVC pattern is a key step in properly factoring your application logic. Well-designed MVC applications have controllers and actions that are small and views that are simple. Keeping your application code DRY (Don't Repeat Yourself) as the application is built is far easier than trying to clean it up later.
Note: |
---|
The routes you create in global.asax.cs define the URL hierarchy of your application. Defining your URL strategy, routes, and controller topology early in a project can help prevent having to change your client application code later. |
Because the majority of the application logic is contained within the models, MVC applications contain different kinds of models:
- View models are built solely for a view to data-bind against. These models are contained within the MVC application and often follow the same composition hierarchy as the views. They are focused on presentation. That is, they are only concerned with presenting data in the UI. Sometimes a special type of view model, called a form model, is also used to represent the data coming into an application from the user.
- Domain models are based on the solution domain. They are focused on handling the business logic of the application. They represent the logical behavior of the application independent of the UI and the storage mechanism. They may be annotated or extended to support some application features such as validation or authentication. Because these models need to be shared between the server and client browser, they are sometimes contained within view models. Domain models are sometimes referred to as application models or service models.
- Data models are built for data services and storage. These are not exposed by the application and are often encapsulated by a services layer.
Organizing your application into these categories is a way of separating concerns in your code. This separation becomes increasingly important as an application grows in complexity. If you find that changes to your application logic are affecting storage or presentation (or vice versa), you should factor the code into separate models.
In some cases, the models may be very similar to one another. In other cases, the models may radically diverge. If your domain model and your data model are very similar, you can consider aggregating an instance of your data model class into your domain model class. If your domain and data models have a matching hierarchy and compatible interfaces, you can also consider using inheritance to derive your domain model classes from your data model classes.
Note: |
---|
Inheritance has the advantage of less coding because you reuse your data model as your domain model, but it is at the cost of tighter coupling. If you can ensure that you will not need to substitute a different data model and that the domain and data models will not diverge, inheritance can be effective. |
As you're writing your controller actions, you should factor complex methods into helper methods or classes in your models and services layer. Use action filter attributes such as HttpPostAttribute to avoid writing conditional logic in each action that inspects the HttpContext. Also, use action filters for cross-cutting concerns such as authentication (for example, AuthorizeAttribute) and error handling (for example, HandleErrorAttribute). Ideally, methods that handle GET should contain only a few method calls and should not contain much conditional logic; methods that handle POST should validate the incoming data, perform the update when the data is valid, and conditionally return a view depending on the success of the update. The following examples from Mileage Stats show two versions of the FillupController's Add method (first the GET version, and then the POST version). In these examples, the generic method Using<T> is a helper method used to delegate logic to the classes in the services layer.
// Contained in FillupController.cs public ActionResult Add(int vehicleId) { var vehicles = Using<GetVehicleListForUser>() .Execute(CurrentUserId); var vehicle = vehicles.First(v => v.VehicleId == vehicleId); var newFillupEntry = new FillupEntryFormModel { Odometer = (vehicle.Odometer.HasValue) ? vehicle.Odometer.Value : 0 }; var fillups = Using<GetFillupsForVehicle>() .Execute(vehicleId) .OrderByDescending(f => f.Date); var viewModel = new FillupAddViewModel { VehicleList = new VehicleListViewModel(vehicles, vehicleId) {IsCollapsed = true}, FillupEntry = newFillupEntry, Fillups = new SelectedItemList<Model.FillupEntry>(fillups), }; ViewBag.IsFirstFillup = (!fillups.Any()); return View(viewModel); }
// Contained in FillupController.cs [HttpPost] [ValidateInput(false)] [ValidateAntiForgeryToken] public ActionResult Add(int vehicleId, FillupEntryFormModel model) { var vehicles = Using<GetVehicleListForUser>() .Execute(CurrentUserId ); if (ModelState.IsValid) { var errors = Using<CanAddFillup>() .Execute(CurrentUserId, vehicleId, model); ModelState.AddModelErrors(errors, "AddFillup"); if (ModelState.IsValid) { Using<AddFillupToVehicle>() .Execute(CurrentUserId, vehicleId, model); TempData["LastActionMessage"] = Resources .VehicleController_AddFillupSuccessMessage; return RedirectToAction("List", "Fillup", new { vehicleId = vehicleId }); } } var fillups = Using<GetFillupsForVehicle>() .Execute(vehicleId) .OrderByDescending(f => f.Date); var viewModel = new FillupAddViewModel { VehicleList = new VehicleListViewModel(vehicles, vehicleId) { IsCollapsed = true }, FillupEntry = model, Fillups = new SelectedItemList<Model.FillupEntry>(fillups), }; ViewBag.IsFirstFillup = (!fillups.Any()); return View(viewModel); }
Note: |
---|
As you're writing your controllers, injecting dependences through the controller's constructor will benefit unit testing. In Mileage Stats, the controller depends on interfaces and not concrete implementations, so we were able to easily replace the actual dependencies with mock implementations. This allowed us to test just the code for the action and not the entire functional stack. For more information, see Chapter 13, "Unit Testing Web Applications." |
After factoring your models and controller actions, your views will use the models to produce HTML. When building views, you should keep the amount of code to a minimum. Code contained in views is not easily testable. Errors in views are harder to debug because the exception occurs during the view engine's rendering pass. However, some very simple logic in views is acceptable. For example, looping over items to build a repeating section of the UI or conditional logic for toggling the visibility of specific sections is fine. Any HTML that is repeated in multiple views is a candidate for being factored into a partial view. However, if you find that you need something more complicated, try to push that logic into the view model. If the logic is a cross-cutting concern, then consider placing that logic inside an HTML helper extension method. Examples of built-in HTML helper extension methods in MVC include BeginForm, RenderPartial, and ActionLink. Examples of helper methods in Mileage Stats are AverageFuelEfficiencyText and AverageFuelEfficiencyMagnitude.
Note: |
---|
The MVC Razor syntax allows you to write code more compactly as well as easily mix code and markup. Don't let this powerful view engine tempt you into writing a lot of code in your views. Instead, let it help you keep the code you do write clear and maintainable. |
Design Checklist for MVC Applications
The following checklist is useful when reviewing your MVC web application code.
Check
|
When reviewing your MVC web application code, ensure that:
|
---|---|
☐
|
Each controller handles a common set of concerns, either for a particular model type or a related set of interactions with the user.
|
☐
|
Action methods consist of a sequence of calls to helper methods, helper classes, and/or model classes. They do not contain complex branching conditional logic. They should be easy to unit test and self-documenting.
|
☐
|
The same code is not repeated in multiple action methods. Action filter attributes are used to handle cross-cutting concerns.
|
☐
|
The majority of the application logic is contained within the model or service layer.
|
☐
|
The hierarchy of model classes used by controller actions and views is effective for the application. If required, separate data model classes are contained within another assembly.
|
☐
|
Views contain only small conditional statements and calls to HTML helper methods.
|
☐
|
The same HTML is not repeated in multiple views. Commonly used HTML is factored into partial views.
|
See the "Further Reading" section for links to more MVC best practices.
Creating a Business Services Layer
As you factor your application code from your controllers' action methods into helper methods and classes, you may find that there are classes and methods that help to properly retrieve, validate, and update data in your data model. This business logic is distinguished from the controller code because it encapsulates logical operations on the domain model and is not specific to any view.
When you have a significant amount of business logic, you may need to create a business services layer. The business services layer is another layer of abstraction, and there is a cost to adding it to the application. However, creating this layer allows you to test the business logic in isolation, and it simplifies the tests for your controllers. Because the business services layer is unaware of the UI, you can also reuse it in the future when exposing additional interfaces such as a web service (using service technologies like Windows® Communication Foundation (WCF)). This allows you to support both desktop and mobile clients from a single business services layer.
When deciding whether or not to create a business services layer, you should also consider whether or not to create a separate domain model. See the section "Factoring Application Code with ASP.NET MVC" for details on the different kinds of models and techniques for separating a domain model from a data model. Creating a separate domain model along with a business services layer is most beneficial when you need to fully encapsulate your data model, your data model does not perform validation, and the domain model functionality will make it easier for you to write your controllers and views. However, having a separate domain model and data model does incur a cost for transforming values between the two models.
The services layer in Mileage Stats consists primarily of handlers and domain models. The handlers are a set of classes that implement the core behavior of the application. They are completely independent from and unaware of the UI. Reading over the names of the handler classes is like reading a list of the features of Mileage Stats. The domain models are a second set of classes in the services layer that differ from both the data models and the view models. The data models in Mileage Stats are primarily concerned with persisting data to the database. The view models are very specific to the needs of the UI. However, the domain models in the services layer are not concerned with either persistence or the UI. The handlers and the domain models in the services layer represent the business logic of the application. Together they provide validation, calculation of statistics, and other services. For more information on data validation, see the "Data Validation" section.
The following illustration shows the high-level design of the services layer and data model.
The following example from Mileage Stats shows the Execute method from the AddFillupToVehicle handler. This handler is represented as a single class with a single public method. We chose the convention of naming the method Execute. The general dependencies of the handler are injected into the constructor of the handler. Any specific values that may be needed to invoke the handler are passed as arguments to the Execute method. Unity is responsible for managing and injecting the dependencies for the handler's constructor, whereas the Execute method will be invoked by some consumer with the necessary arguments. In the case of Mileage Stats, the consumer is a controller action.
Also note that the handler has two private helper methods: ToEntry and AdjustSurroundingFillupEntries. ToEntry is responsible for converting the data to the form needed by the data layer. CalculateInterFillupStatistics (which is called within AdjustSurroundingFillupEntries) is responsible for calculating the statistics.
// Contained in AddFillupToVehicle.cs public virtual void Execute(int userId, int vehicleId, ICreateFillupEntryCommand newFillup) { if (newFillup == null) throw new ArgumentNullException("newFillup"); try { var vehicle = _vehicleRepository.GetVehicle(userId, vehicleId); if (vehicle != null) { newFillup.VehicleId = vehicleId; var fillup = newFillup; var entity = ToEntry(fillup); AdjustSurroundingFillupEntries(entity); _fillupRepository.Create(userId, vehicleId, entity); // update calculated value newFillup.Distance = entity.Distance; } } catch (InvalidOperationException ex) { throw new BusinessServicesException(Resources .UnableToAddFillupToVehicleExceptionMessage, ex); } }
In Mileage Stats, the handlers are responsible for implementing the core business logic of the application. The controllers have the responsibility of accepting the user's input and invoking the handler. Controllers then take the results of invoking handlers and compose any data necessary for rendering views. This data frequently takes the form of classes that we call view models.
Overall, the business services layer provides functionality that makes writing controllers, actions, and views much easier.
Supporting Interactive Web Clients
Interactive web clients communicate asynchronously with the server and manipulate the document object model (DOM). Because multiple interactions can occur simultaneously, managing state and tracking events can be difficult. This section outlines ways the web application server can support web clients by providing services that reduce the complexity of the JavaScript.
Providing HTML Structure
Traditionally, the server in a web application returns HTML as content that the client's browser renders directly. Because interactive web clients manipulate the HTML structure, you will need to focus less on the appearance of the HTML and more on providing a useful hierarchy for the client. You should think of the HTML structure as part of the contract between the client and the server.
In order to modify the content, web clients first need to locate elements in the DOM. The jQuery library provides a selector syntax that can be used to locate elements in many ways (such as by ID, class, relative position, and so forth). If the web client depends on the hierarchical structure of the HTML you produce, you will likely break the client application when you modify the structure.
To avoid tightly coupling the client JavaScript with the HTML structure, you can use data- (pronounced "data dash") attributes in your HTML. The data- attributes are attributes whose names are prefixed with "data-" and represent metadata on elements.
Many JavaScript developers use the id and class attributes to locate elements. The id attribute is limited because there can be only one per element and id values are generally expected to be unique within a page. The class attributes cause confusion because they are also used to apply layout and style to the element through Cascading Style Sheets (CSS).
Because data- attributes are independent of the HTML structure, they allow you to restructure the HTML without impacting the client. See Chapter 6, "Client Data Management and Caching" and "Using Data- Attributes" in Chapter 7, "Manipulating Client-Side HTML" for more information on how clients can use data- attributes.
Below are two data- attribute examples from Mileage Stats. In the first example, the data-vehicle-id attribute allows the client to locate the associated element. Notice that we are rendering the value for the data- attribute on the server and that it will be consumed by JavaScript on the client.
// Contained in Views\Vehicle\List.cshtml <a class="list-item @(item.Reminder.IsOverdue ? "overdue" : null)" href="@Url.Action("Details", "Reminder", new { id = item.Reminder.ReminderId })" data-vehicle-id="@item.Vehicle.VehicleId"> ... </a>
In the second example, the data-chart-url attribute provides the client a URL to use in an Ajax call.
// Contained in Views\Vehicle\List.cshtml <div id="main-chart" class="article framed" data-chart-url="@Url.Action("JsonGetFleetStatisticSeries", "Home")">
Ideally, your JavaScript should use only data- attributes to locate elements and to discover contextual data from the server. However, there are cases where using a selector to manipulate all elements having a given element name, unique ID, or class is a more practical approach. In both cases, you should write the JavaScript code to allow for the case where the set of selected elements is empty.
Note: |
---|
If you have a team of developers writing the web client independently of the web application, we strongly recommend you ensure agreement on the expected HTML structure before writing the web client JavaScript. |
Using View Model and View Bag
ASP.NET MVC 3 introduced the ViewBag. A ViewBag is a dynamic object that wraps the ViewData property that was present in previous versions of ASP.NET MVC. A ViewBag is a name/value keyed collection that lets you store loosely typed data. This differs from the Model property on the View, which contains strongly typed data. Having two ways to provide the view data can cause confusion about when to use View.Model versus ViewBag.
The strongly typed View.Model property has several benefits over ViewBag. It enables IntelliSense® auto-complete in the view, and it provides type safety when generating the view model from a controller action. In addition, many of the helpers are specifically designed to work with a strongly typed model, and they can extract metadata from the model to help automatically construct a view.
When you use the View.Model in a form element, you will have an associated controller action (marked with the HttpPostAttribute) that accepts the model as a parameter. When the form is submitted, the MVC model binder uses the posted form data to construct and populate an instance of your view-model class.
Often the view model representing the form that is passed into a controller action will be significantly different from the view model returned from the controller action. In those cases you can create a form model that contains only the data from the form. An example of this in Mileage Stats is the Add action on VehicleController. It has a parameter of type VehicleFormModel and returns a view model of type VehicleAddViewModel. The VehicleAddViewModel contains data such as the current user and a list of vehicles, as well as the original form model.
If possible, create a view model for each of your views. This gives you complete control over the data sent to and from the client. It also reduces confusion by making the relationship between views and view models explicit. Likewise, using form models that are specific to views prevents the ASP.NET MVC model binder from setting properties that you didn't expect to receive from the client. In many cases, if you follow this practice you will never need to use ViewBag.
However, there can be cases when your view needs additional data that doesn't belong in your view model and you don't want to make a round trip to the client. In these cases, consider placing the data in ViewBag.
In Mileage Stats, the _ProfileForm partial view uses the User class as the View.Model. Part of the view is a drop-down list of countries. The following example shows the ViewBag used to populate the drop-down list.
//Contained in Views\Shared\_ProfileForm.cshtml @model MileageStats.Domain.Models.User ... <div class="editor-label"> @Html.LabelFor(model => model.Country) </div> <div class="editor-field"> @Html.DropDownListFor(model => model.Country, ViewBag.CountryList as SelectList, "-- Select country --", new { @class = "editor-textbox" }) @Html.ValidationMessageFor(model => model.Country) </div> ... <div class="editor-commands"> <button data-action="profile-save" class="button generic small editor-submit" type="submit"> <img src="@Url.Content( "~/Content/button-save.png")" title="Save Profile" alt="Save" /> </button> </div> <div style="clear: both;"> </div> @Html.ValidationSummary(true)
Mileage Stats could have had a separate view-model class containing the User and an ICollection<Country> object. However, doing so would make the partial view less reusable because every view model in the hierarchy of views and partial views would have to contain the new view model.
Providing Data Asynchronously
Requesting data asynchronously is at the heart of a responsive, interactive web client. One option for requesting data asynchronously is to use WCF to expose a service API for the client. This is a good choice when the application is not responsible for serving the web UI. ASP.NET MVC web applications are a great endpoint for serving data to web clients when the application must also serve the site's UI. You can use the same routing, controllers, security, and models for returning data that you use for returning HTML structure. This allows the web client to use the relative URLs you provided in the data- attributes as well as some knowledge of the site's URL structure to create requests for data.
Choosing a Data Format
Web clients typically request data as HTML, JavaScript Object Notation (JSON), XML, or binary (for example, images and video) from the server. Each of these formats has its unique strengths.
The JSON format is recommended when the web client needs to bind data to existing HTML elements, generate new HTML from the data, transform the data, or make logical decisions based on the data. JSON is a very concise format that has serialization support on the client and in ASP.NET MVC. Because JSON contains no markup, it helps separate UI and data service concerns.
The HTML format is useful when the client makes minimal or no changes to the returned content and simply places the entire HTML result into a pre-determined area of the page. Examples of where HTML works well are advertisements, content aggregators, and content management systems.
The XML format is useful when the client receives data based on a pre-defined schema. XML is also used when working in open-standards formats such as RSS, Atom, and oData. Web clients can use the known schema structure to process the XML into HTML.
Binary formats are generally employed for media. Images are the most common example; the server returns an img element with a src attribute, the browser makes a secondary request to the server and then renders the binary result as an image.
Note: |
---|
Not all browsers send the same data to the server when requesting images and other resources. Some browsers will send authentication headers and cookies while others will not. If you have secondary requests that must be authenticated, you need to verify that those requests work on the browsers you intend to support. In addition, you should perform testing in both the ASP.NET development server and in a Microsoft Internet Information Services (IIS)-deployed web application. |
To support a particular format in ASP.NET MVC, return a JsonResult, ContentResult, or FileResult instead of a ViewResult from your action methods.
The following example from Mileage Stats returns a JsonResult. The view model is created and then the Controller.Json method is called to convert the object into JSON for the response.
// Contained in VehicleController.cs [Authorize] [HttpPost] public JsonResult JsonDetails(int id) { VehicleModel vehicle = Using<GetVehicleById>() .Execute(CurrentUserId, vehicleId: id); // we are limiting this to 3 reminders // after we retrieve the full set from the server var overdue = Using<GetOverdueRemindersForVehicle>() .Execute(id, DateTime.UtcNow, vehicle.Odometer ?? 0) .Take(3); var vm = ToJsonVehicleViewModel(vehicle, overdue); return Json(vm); }
Note: |
---|
Controller actions that return a JsonResult are easy to unit test because you can inspect the JsonResult.Data property directly. However, debugging a serialization issue with a JsonResult is harder because it requires you to inspect the returned data from the web service in the web client. |
Factoring Controller Actions for Ajax
The same set of principles used to build controller actions in general still apply when those actions return JSON. If you decide to create a separate set of URLs for returning data (for example, if you create an independent data service API), you can create separate controllers and routes. This approach is beneficial when you expect multiple types of clients (for example, a web client and a Microsoft Silverlight® browser plug-in) to use the data actions, but only the web client to use the view-based actions.
If your data actions are closely related to your view-based actions, you can put data actions in the same controller as the view-based actions. Mileage Stats is an example of this scenario because the data actions focus on the same domain models as the view-based actions.
If your web client needs to use the same URL to request different data formats, you can extend your controller action methods by using the HttpRequestBase.IsAjaxRequest extension method to determine which to determine the format of the request and the data format to return. This is beneficial when you can reuse your view model as your JSON model. If you find that you have large if-else blocks in your controller actions, you should factor the view-based and JSON actions into different helper methods. Alternatively, you can write a custom AjaxAttribute action filter that uses IsAjaxRequest. This action filter would provide overloaded action methods similar to those in HttpPostAttribute.
When errors occur, your data actions can throw exceptions just like view-based actions do. The jQuery library supports beforeSend, send, success, error, and complete handlers to handle server responses and failures. If you don't want the friendly error page HTML content returned when a JSON data action throws an exception, you may need to apply a different HandleErrorAttributevalue to your data actions.
As you design your data actions, you should consider how many round trips to the server will be required for each interaction with the user. Every Ajax request requires separate threading on the client as well as resources for the connection, server response, data download, and client processing. If you create overly granular data actions, your web client may encounter performance issues because it must manage a large number of requests in order to satisfy a user action. If you create very large data actions, your web client and server may encounter performance issues because of both the creation and processing of large amounts of data.
Note: |
---|
You may find it helpful to use web browser debugging and tracing tools such as Windows Internet Explorer® F12 developer tools, Fiddler, and FireBug to see the relative cost of the different parts of each round trip to the server. Depending on the connection speed and distance between your users and your server, creating a connection can be much more costly than downloading the data once a connection is made. Many web applications that are used globally are designed to request larger chunks of data when that data cannot be cached closer to the user. |
Data Validation
Interactive web applications need to let users know when they have provided data that is invalid. Data validation checks need to happen in three places: on the client in the context of what the user is trying to accomplish, on the server to protect from untrustworthy callers, and in the database to ensure data integrity. Having data validation occur at multiple levels in the stack makes creating common and consistent validation logic important to the user experience. This section covers data validation techniques you can use to validate your data on both the server and the client.
Data Annotation Attributes
Applying data annotation attributes to your model allows ASP.NET MVC and the ADO.NET Entity Framework to provide data validation at the server level. As mentioned in the "Creating a Data Model" section, the ADO.NET Entity Framework also inspects the data annotation attributes on your entity classes in order to create the database schema. You can find the standard data annotation attributes in the System.ComponentModel.DataAnnotations namespace. In this section, data annotation attributes that provide validation are referred to as validation attributes.
The following example shows the validation attributes applied to the VehicleFormModel class in the MileageStats.Domain project. The attributes applied to the VehicleFormModel.Name property validate that the name is not null, is not an empty string, is no more than 20 characters, and does not contain script injection characters.
// Contained in VehicleModel.cs [StringLength(20, ErrorMessageResourceName = "VehicleNameStringLengthValidationError", ErrorMessageResourceType = typeof(Resources))] [TextLineInputValidator] [Required(AllowEmptyStrings = false, ErrorMessageResourceName = "VehicleNameRequired", ErrorMessageResourceType = typeof(Resources))] public string Name { get { return this.Vehicle.Name; } }
Validation attributes also support localization. By using the resource names, the error messages are loaded from a RESX file.
Validating Data in MVC
The ASP.NET MVC default model binder uses the Validator and ValidationContext classes when parsing incoming data into an instance of your model class. These two classes work together to validate the data based on the validation attributes you have applied.
If any of the validation fails, AddModelError is called on the ModelState class. ModelState.IsValid returns false when ModelState has one or more errors. Because all of this happens before your action is called, validating data in your controller actions is easy. The following example shows the FillupController using ModelState.IsValid before making the update.
// Contained in FillupController.cs [HttpPost] [ValidateInput(false)] [ValidateAntiForgeryToken] public ActionResult Add(int vehicleId, FillupEntryFormModel model) { var vehicles = Using<GetVehicleListForUser>() .Execute(CurrentUserId ); if (ModelState.IsValid) { var errors = Using<CanAddFillup>() .Execute(CurrentUserId, vehicleId, model); ModelState.AddModelErrors(errors, "AddFillup"); if (ModelState.IsValid) { Using<AddFillupToVehicle>() .Execute(CurrentUserId, vehicleId, model); TempData["LastActionMessage"] = Resources .VehicleController_AddFillupSuccessMessage; return RedirectToAction("List", "Fillup", new { vehicleId = vehicleId }); } } ... var viewModel = new FillupAddViewModel { ... }; return View(viewModel); }
In the previous example, invoking the handler CanAddFillup returns a ValidationResult collection. These validation results are returned from the business services layer. The AddModelErrors extension method iterates over the ValidationResult collection and calls ModelState.AddModelError for each. This level of indirection keeps the business services layer from depending on ASP.NET MVC.
Note: |
---|
The previous example turns off the ASP.NET-provided input validation of cookies, form data, and querystring properties by using the [ValidateInput(false)] attribute. This was done to allow certain characters that the default input validation does not allow. However, to prevent the user from using these characters to introduce malicious data into the system, theTextLineInputValidator data validation attribute is applied to the text properties of the FillupEntryFormModel class. It is recommended that you be very careful when disabling the default input validation and ensure that there are mitigations in place to stop malicious input. |
Creating Custom Validation Attributes
When the standard validation attributes don't provide what you need, you can write your own. All the standard validation attributes derive from the ValidationAttribute class that contains the abstract IsValid method.
The following example validates a postal code. The implementation is much simpler than would be used in many applications, but it shows how a model object can validate one field based on the value of a different field.
// Contained in PostalCodeValidatorAttribute.cs protected override ValidationResult IsValid(object value, ValidationContext context) { var userToValidate = context.ObjectInstance as User; var memberNames = new List<string>() { context.MemberName }; if (userToValidate != null) { if (string.IsNullOrEmpty(userToValidate.Country) && string.IsNullOrEmpty(userToValidate.PostalCode)) { return ValidationResult.Success; } if (string.IsNullOrEmpty(userToValidate.PostalCode)) { return ValidationResult.Success; } if (userToValidate.Country == Resources.UnitedStatesDisplayString) { if (USPostalCodeRegex.IsMatch(userToValidate.PostalCode)) { return ValidationResult.Success; } return new ValidationResult(Resources.USPostalCodeValidationErrorMessage, memberNames); } else { if (InternationalPostalCodeRegex.IsMatch(userToValidate.PostalCode)) { return ValidationResult.Success; } return new ValidationResult( Resources.InternationalPostalCodeValidationErrorMessage, memberNames); } } return ValidationResult.Success; }
Handling Complex Data Validation
You may have noticed in the earlier example that the FillupController.Add method calls ModelState.IsValid twice. The CanAddFillup handler in the following example contains validation logic that uses multiple objects in the domain model and requires additional database access. This validation logic is not suited for a single custom ValidationAttribute. It returns a collection of validation results that the controller uses to call ModelState.AddModelError. In cases like these you should factor complex validation logic into helper methods or a business services layer.
// Contained in CanAddFillup.cs public virtual IEnumerable<ValidationResult> Execute(int userId, int vehicleId, ICreateFillupEntryCommand fillup) { var foundVehicle = _vehicleRepository.GetVehicle(userId, vehicleId); if (foundVehicle == null) { yield return new ValidationResult(Resources.VehicleNotFound); } else { var fillups = _fillupRepository.GetFillups(vehicleId); if (!fillups.Any()) yield break; var priorFillup = fillups .Where(f => f.Date < fillup.Date) .FirstOrDefault(); if ((priorFillup != null) && (priorFillup.Odometer >= fillup.Odometer)) { yield return new ValidationResult( "Odometer", string.Format(CultureInfo.CurrentUICulture, Resources.OdometerNotGreaterThanPrior, priorFillup.Odometer)); } } }
Supporting Validation on the Client
ASP.NET MVC supports client-side validation of data by sharing validation information from the server. This is done by implementing the IClientValidatable interface on your custom validation classes. IClientValidatable contains only the GetClientValidationRules method that returns a ModelClientValidationRule collection.
In the following example, the PostalCodeValidatorAttribute implements GetClientValidationRules by returning a single ModelClientValidationRule. By setting the ValidationType property to "postalcode" the client will use the validation routine with the same name that is registered on the client. The validation parameters are added to provide the client-side code with the information it needs to implement the validation rule.
// Contained in PostalCodeValidatorAttribute.cs public IEnumerable<ModelClientValidationRule> GetClientValidationRules( ModelMetadata metadata, ControllerContext context) { var rule = new ModelClientValidationRule() { ErrorMessage = Resources.InvalidInputCharacter, ValidationType = "postalcode" }; rule.ValidationParameters.Add("internationalerrormessage", Resources.InternationalPostalCodeValidationErrorMessage); rule.ValidationParameters.Add("unitedstateserrormessage", Resources.USPostalCodeValidationErrorMessage); rule.ValidationParameters.Add("internationalpattern", Resources.InternationalPostalCodeRegex); rule.ValidationParameters.Add("unitedstatespattern", Resources.USPostalCodeRegex); return new List<ModelClientValidationRule>() { rule }; }
Note: |
---|
In the previous example, you can see that the validation type and the names of the validation parameters are lowercase. These values are converted into HTML attributes and used by the client-side JavaScript code. Due to the limitations of this transformation, these values must contain only numbers, lowercase letters, and dashes. |
When MVC HTML helper extension methods such as TextBoxFor and EditorFor are called, MVC inspects the property definition for validation attributes. When a validation attribute implements IClientValidatable, MVC uses the client validation rules to include data-val attributes. The following HTML fragment shows the data-val attributes present on the postal code field in the registration form.
<!-- sent from server using Views/Shared/_ProfileForm.cshtml --> <div class="editor-field"> <input data-val="true" data-val-length="Postal code must be less than 10 characters." data-val-length-max="10" data-val-postalcode="Only alpha-numeric characters and [.,_-&#39;] are allowed." data-val-postalcode-internationalerrormessage= "Postal codes must be alphanumeric and ten characters or less." data-val-postalcode-internationalpattern="^[\d\w]{0,10}$" data-val-postalcode-unitedstateserrormessage= "United States postal codes must be five digit numbers." data-val-postalcode-unitedstatespattern="^[\d]{5}$" data-val-textlineinput= "Only alpha-numeric characters and [.,_-&#39;] are allowed." data-val-textlineinput-pattern="^(?!.*--)[A-Za-z0-9\.,'_ \-]*$" id="PostalCode" maxlength="10" name="PostalCode" size="10" type="text" value="" /> <span class="field-validation-valid" data-valmsg-for="PostalCode" data-valmsg-replace="true"> </span> </div>
The JavaScript that participates in MVC validation on the client side can be found in jquery.validate.js and jquery.validate.unobtrusive.js. These files include standard validation attributes, methods to register validation routines, and unobtrusive validation adapters. The following example shows the registration of the postalcode validation routine. Notice how it uses the params object to access the data-val attributes.
// Contained in mstats.validation.js $.validator.addMethod('postalcode', function (value, element, params) { if (!value) { return true; // not testing 'is required' here! } try { var country = $('#Country').val(), postalCode = $('#PostalCode').val(), usMatch = postalCode.match(params.unitedStatesPattern), internationalMatch = postalCode.match(params.internationalPattern), message = '', match; if (country.toLowerCase() === 'united states') { message = params.unitedStatesErrorMessage; match = usMatch; } else { message = params.internationalErrorMessage; match = internationalMatch; } $.extend($.validator.messages, { postalcode: message }); return (match && (match.index === 0) && (match[0].length === postalCode.length)); } catch (e) { return false; } });
IClientValidatable helps you to share validation information, but you still have two copies of your validation logic to maintain. You may choose remote validators (that is, by implementing validation actions in your controller) and call them using Ajax from the client. However, the round trip to the server will not be as responsive as validating directly on the client.
Note: |
---|
It is important to remember that client-side validation only helps improve the user experience and is not a substitute for proper validation and security on the server. Hackers won't use your web client JavaScript or even the browser when maliciously posting data to the web application on the server, so you must ensure that any client-side validation is repeated on the server before any data changes occur. |
Unit Testing Validation Logic
Because validation occurs at multiple levels of the stack, you may end up with duplicate validation attributes and validation logic to keep in sync. While proper factoring of your application logic, your models, and data validation information can help, you should always unit test each layer in isolation to make sure the validation works as expected.
While you don't need to unit test the standard validation attributes, you should test that the validation attributes are properly applied to your model and validate as expected (just as if you had written code inside the setter of your model property). The following example shows a unit test verifying that the Title of the Reminder is required.
// Contained in ReminderFixture.cs [Fact] public void WhenTitleSetToNull_ThenValidationFails() { Reminder target = new Reminder(); target.Title = null; var validationContext = new ValidationContext(target, null, null); var validationResults = new List<ValidationResult>(); bool actual = Validator.TryValidateObject(target, validationContext, validationResults, true); Assert.False(actual); Assert.Equal(1, validationResults.Count); Assert.Equal(1, validationResults[0].MemberNames.Count()); Assert.Equal("Title", validationResults[0].MemberNames.First()); }
Note: |
---|
The true parameter at the end of the TryValidateObject call is important because it causes the validation of all properties on the target. This means your unit test must ensure all other properties are set to valid values when you verify that one invalid property fails validation. |
Other Considerations
This section describes some other areas of server architecture you may want to consider.
Dependency Injection
Mileage Stats uses Unity for dependency injection. The unity.config file in the web application maps interfaces to concrete classes. It also determines the lifetime for each mapping. For example, Unity ensures that the VehicleController constructor receives implementations of the IUserServices, ICountryServices, IServiceLocator, and IChartDataService interfaces.
In an effort to manage dependencies and improve testability in the MVC pattern, ASP.NET MVC also provides a dependency resolver. This gives ASP.NET MVC applications a designated place to resolve dependencies for framework-created objects such as controllers and action filters. In the following example, Mileage Stats registers Unity as the MVC dependency resolver as part of initializing the dependency injection container for the application.
// Contained in global.asax.cs private static void InitializeDependencyInjectionContainer() { IUnityContainer container = new UnityContainerFactory() .CreateConfiguredContainer(); var serviceLocator = new UnityServiceLocator(container); ServiceLocator.SetLocatorProvider(() => serviceLocator); DependencyResolver.SetResolver(new UnityDependencyResolver(container)); }
See the "Further Reading" section for more information on dependency injection and Unity.
Unit Testing
One of the main reasons ASP.NET MVC follows the MVC pattern is to allow for unit testing of the application logic. The System.Web.Abstractions assembly was introduced primarily to allow substitution of mocked instances of classes like HttpContextBase during unit testing. You should unit test as much of your application logic as possible; it will not only help ensure the quality of your application but will also help identify design issues early when they are less expensive to fix.
Mileage Stats uses the xUnit.net unit test framework as well as Moq for mocking interfaces. The application is unit tested at the data model, business services, and controller layers. As mentioned in the "Composing Application Logic" section, keeping controller actions simple and factoring application logic into a business services layer makes unit testing much easier. In Mileage Stats, unit testing was much easier because interfaces like IUserServices could be mocked.
For more information on unit testing see Chapter 13, "Unit Testing Web Applications."
Error Management
Web clients expect proper HTTP status code responses when a web application cannot fulfill a request. This means you should avoid hiding errors like a resource not being found (404), failure to authorize (403), and internal server errors (500+). When an exception is thrown from a controller action, ASP.NET MVC will respond with the correct HTTP status code. There may be cases where you need to catch an exception from a call and throw a different exception type.
Generally, users don't want to see all the developer details for an exception, and showing more than is needed is not recommended from a security standpoint. ASP.NET MVC provides a HandleErrorAttribute that provides a friendly error page when an exception occurs. The friendly page displayed is determined in the web.config customErrors section. Mileage Stats applies theHandleErrorAttribute to all controller actions in the RegisterGlobalFilters method in Global.asax.cs.
Although friendly errors are an improvement, the user experience shouldn't be interrupted with an HTTP error if the user enters an invalid value. Use the Post/Redirect/Get pattern (PRG) when handling a POST action. When the user submits invalid data, you should return the same view as the GET action populated with the incoming data. When a POST action succeeds, it can redirect.
In the following example, if a ReminderFormModel doesn't pass data validation, the Add view result is returned populated with the reminder data that was passed into the action method.
//Contained in ReminderController.cs [HttpPost] public ActionResult Add(int vehicleId, ReminderFormModel reminder) { if ((reminder != null) && ModelState.IsValid) { var errors = Using<CanAddReminder>() .Execute(CurrentUserId, reminder); ModelState.AddModelErrors(errors, "Add"); if (ModelState.IsValid) { Using<AddReminderToVehicle>() .Execute(CurrentUserId, vehicleId, reminder); return RedirectToAction("Details", "Reminder", new { id = reminder.ReminderId }); } } var vehicles = Using<GetVehicleListForUser>() .Execute(CurrentUserId); var vehicle = vehicles.First(v => v.VehicleId == vehicleId); var reminders = Using<GetUnfulfilledRemindersForVehicle>() .Execute(CurrentUserId, vehicleId, vehicle.Odometer ?? 0) .Select(r => new ReminderSummaryModel(r, r.IsOverdue ?? false)); var viewModel = new ReminderAddViewModel { VehicleList = new VehicleListViewModel(vehicles, vehicleId) { IsCollapsed = true }, Reminder = reminder, Reminders = new SelectedItemList<ReminderSummaryModel>(reminders), }; return View(viewModel); }
Concurrency
Because Mileage Stats tracks vehicles per user account, concurrency conflict detection and management was not a scenario for the application. Even though we chose not to implement it, the ADO.NET Entity Framework does support optimistic concurrency by adding time stamps to the data model and taking appropriate action when handling the DbUpdateConcurrencyException.
Summary
Hopefully you now have a frame of reference for architecting your server-side web application. There are many choices you will make to structure your site, factor code, and model data. Successful architectures provide the layers of abstraction required to solve the problem at hand while affording a way to accommodate future features and technologies.
The key points made in this chapter are:
- Understand your web client needs and build a contract for the HTML structure, URL structure, and data formats between the client and server early in the process.
- Decide whether or not to create a business services layer and whether or not to create separate domain and data models.
- Create small controllers by placing the majority of your application logic in your domain models, a services layer, or helper classes and methods.
- Keep application logic simple and partitioned.
- Provide a data API that allows web clients to consume data asynchronously in the right format and with the right granularity for the application.
- Structure your validation logic to support validation both on the client and on the server.
No comments:
Post a Comment