A N-Tier Architecture Sample with ASP.NET MVC3, WCF, and Entity Framework
Reference - http://www.codeproject.com/Articles/434282/A-N-Tier-Architecture-Sample-with-ASP-NET-MVC3-WCF
This article tries to introduce a decoupled, unit-testable, deployment-flexible, implementation-efficient and validation-flexible N-Tier architecture in .NET
Contents
- Overview
- Using The Code
- Overview of All Layers
- Main Achievements in the Sample N-Tier Application
- Visual Studio 2010 Projects in The Sample Application
- Fold Structure and its Matching with Layers
- Summary of The Main Folders and Projects
- Elaboration of Several Main Component Groups and Projects
- Folder Framework in project GH.Common
- Project GH.Northwind.Persistence
- Project GH.Northwind.EntityFramework
- Project GH.Northwind.EntityFramework.Host
- Projects GH.Northwind.Business.Entities, GH.Northwind.Business.Interfaces and GH.Northwind.Business
- Project GH.Northwind.Business.Host
- Project GH.Northwind.Client.Common
- Project GH.Northwind.Web
- Discussion
- Possible Configuration Results and Analysis of N-Tier Architectures From Diagram 1
- Additional Advantages of N-Tier Architecture Sample Besides the General Advantages
- Separation of WCF Implementation from WCF Host
- Purpose of Library Project GH.Northwind.Persistence
- Why Don't We Use the Domain Service Class Feature for Business Layer?
- Why Don't Client Sides Call WCF Data Service Directly but Through Business Layer?
- Business Entity Classes and Code Generators
- Data Validation
- Some Configuration Results of the Sample N-Tier Architecture Application
- Conclusions
Overview
N-Tier software architecture can solve the following client/server system issues: scalability, security, fault tolerance and etc. In our previous article "N-Tier Architecture and Tips" we introduce the basic N-Tier architecture concepts and some practical tips. In this article, we try to elaborate a N-Tier architecture sample with ASP.NET MVC3, WCF and Entity Framework. In Java, usually there is a pre-defined way to achieve the N-Tier architecture: J2EE architecture, which uses session bean over entity bean for business and persistence layer, Java bean, servlet or JSP for the client presenter layer, Java Swing, HTML or applet as the client side. Therefore, for J2EE, different application are very likely implemented in very similar ways. However, in .NET, even though there are many tools and features available, there isn’t any pre-defined way as J2EE does to guard how to implement the N-Tier architecture. As a result, there are too many inconsistent and existing ways to do this. Some are good; some are bad. This article tries to introduce a decoupled, unit-testable, deployment-flexible, implementation-efficient and validation-flexible N-Tier architecture in .NET. What we achieved here is to put some well-known nice tools and features in .NET together and come up with a workable solution. Because there is too much to cover in one article, we will mainly concentrate on business and persistence layers of N-Tier architecture in our sample solution, but will still briefly touch other layers too. In order to understand better this article, we suggest you to read first our previous article on the basics of N-Tier architecture here. As our previous article, this article is also based on the assumption that a team has a full control over all layers of the N-Tier architecture.Using the code
The code is created in Visual Studio 2010. Following steps are needed before you build and run this project. 1) Install the database Northwind in SQL server; I used SQL server expression 2008R2; other version should be fine too. Open the script file \GH.3Tier.Demo\instnwnd.sql in Sql server management Studio, then execute it. 2) update the data source for the NorthwindEntities database connectionString in the config files of three application projects GH.Northwind.Web, GH.Northwind.Business.Host and GH.Northwind.EntityFramework.Host with your own sql server name. 3) If the combination result of parameters "N-Tier" and "UseWcfDataservice" in the configuration file of project GH.Northwind.Web and parameter "UseWcfDataservice" in project GH.Northwind.Business.Host will need a WCF service or WCF data service, we will need to launch wcf project GH.Northwind.Business.Host locally by right-clicking the project in solution explorer of Visual Studio, then click View in Browser. Also we can launch the wcf data service project GH.Northwind.EntityFramework.Host locally by the similar way; see table 2 later for some possible combination results. Currently, we only implement CRUD operations for entity Product in the ASP.NET MVC3.Disclaimer
The sample code is solely based on my self-study and research results, not based on any practical project. The database Northwind used in this sample is the Microsoft famous public database, which originally comes with every Microsoft Access database and some old SQL servers. The code is just for the purpose to explain the overall architecture only, so it is very primitive and far away from completion. If you want to use the code in your project, you need to enrich and modify it to meet your own need. Also, the code hasn’t been unit-tested yet, feel free to correct any error that you meet. For example, practically, you may only need to implement one type of persistence layer if your business will be for sure to stick to one type persistence technique only, either DataServiceContext, ObjectContext or DbContext. In our sample demo project, we implement all of DataServiceContext, ObjectContext and DbContext to explain that we can switch the persistence techniques easily without the need to modify the business layer. Another example, if you don’t want library GH.Northwind.Business to be deployed in the same web server computer as client presenter layer for security, you can remove its reference and code from project GH.Northwind.Client.Common. In this way, you are for sure that you can always deploy client presenter layer and business layer in separate machines for security. In our sample project, we try to explain our deployment flexibility by allowing the business layer to be able to deploy in the same computer as client presenter layer; this deployment also exists in many practical cases. In addition, the CRUD operations in the sample project is very primitive with basic features, you can enrich them with some advanced features, such as many-to-many relationship and many-to-one relationship handling and etc.Overview of All Layers
Below is the diagram for the N-Tier architecture model in .NET that will be discussed in this article:Diagram 1: N-Tier architecture in .NET
The oval components in each layer may co-exist or exist individually only. We summary all layers as below.
Client layer: this layer is involved with users directly. There may be several different types of clients coexisting, such as WPF, Window Form, HTML web page and etc.
Client presenter layer: contains the presentation logic needed by clients, such as ASP .NET MVC in IIS web server. Also it adapters different clients to the business layer. There are three components drawn in this layer: common lib, ASP .NET, WPF client lib. Common lib holds reusable common code for all types of clients. ASP .NET lib and WPF client lib are libraries individually for web client and WPF client. If there is a new type of client added, we may need to add additional lib to support the new type client only, such as Windows Form client.
Business layer: handles and encapsulates all of business domains and logic; also called domain layer. There are two components in this layer: WCF lib and WCF host application; all business logic and implementation are in WCF lib; WCF host application only deal with the WCF deployment. In a 2-tier architecture configuration, client presenter layer will call the WCF lib directly; whereas, in a complete N-Tier architecture configuration, client presenter layer will call the WCF host application directly. The business interface in this layer is usually a business oriented facade layer which exposes the business operations to the clients at the degree just to meet the client’s need. Therefore, it also acts as a layer of proxy to the client side to protect the business domain and logic. Client sides usually see the business layer as a black box: send a request to the business layer, then the request gets done and responded; the client sides usually don't know how their requests are fulfilled.
Persistence layer: handles the read/write of the business data to the data layer, also called data access layer (DAL). There are three components in this layer: Entity Framework lib, persistence lib and optional WCF data service. Persistence lib is a generic library to facilitate and simplify the usage of Entity Framework or other persistence technique in business layer; it also decouples the business layer from Entity Framework. Business layer will call directly the persistence library other than Entity framework. Depending on the application configuration, the persistence lib will then either call Entity Framework lib directly or call directly the WCF data service which is created on the top of the Entity Framework lib. If the database is serving as a data center for a variety of different types of applications, WCF data service will be a good choice; otherwise, we may need to avoid WCF data service to gain performance.
Data layer: the external data source, such as database server, CRM system, ERP system, mainframe or other legacy systems and etc. Data layer is used to store the application data. Database server is the most popular nowaday. We list three modern databases in Diagram 2 for selection.
Main Achievements in the Sample N-Tier Application
We did following main things in our sample application:- A layer depend on another layer by interfaces only, not by concrete classes.
- Different tier architectures can be switched easily simply by updating two
bool
parameter’s values in configuration files: parameters "UseWcfDataService" and "N-Tier". We achieve this by separating WCF implementation from WCF Host into different projects, and also by the non-proxy way of the client side to access WCF services. - To use them in the whole application, we auto-generate and maintain only one version of entity classes with the T4 template in a lightweight library project GH.Northwind.Business.Entities other than the original Entity Framework project so that we can regenerate and reuse everywhere these auto-generated entity classes from a lightweight library other than a place cluttered with the heavyweight Entity Framework stuff.
- Business layer doesn’t call Entity Framework directly, but through Persistance lib. Doing so helps us to achieve the maximum decoupling between business layer and persistence layer and allows us to swap the persistence technologies easily without any side effect on the business layer. Also, it facilitates and simplifies the usages of Entity Framework in business layer. The sample application demonstrates that we can swap the persistence technologies between WCF data service and DbContext (or: ObjectContext) easily by just updating the value of bool parameter "
UseWcfDataService
" in the configuration file. - For data validation in our sample application, we use the auto-generated metadata classes for the simple validation for individual properties and interface IValidatableObject for class-level validation crossing multiple properties. Currently, we put all validation logic in one place: project GH.Northwind.Business.Entities so that we only need to implement and maintain one version of validation logic. If somehow for security reason in certain cases, we can move all validation logic into its own library project. All layers share this version of validation logic. We can call these validations easily in many layers, based on our practical needs. Currently, we call these validations in the html webpage client layer and the ASP .NET MVC3 client presenter layer.
- For error handling in this WCF business service layer, we implement IErrorHandler behavior to catch all exceptions and convert them into FaultException so that the service channel won’t be faulted out and can be used again after an exception occurs. Also, the FaultException can be sent to the client side to help debugging.
- Try to auto-generate code as much as possible: all entity classes with wcf tags and metadata classes with annotation tags are automatically generated by code generators. We even auto-generate the draft version of the business interface by domain service class wizard.
- We use the non-proxy way in client side to access WCF service and WCF data service. Doing so can allow us to use data contracts (mainly the auto-generated entity classes) and service interfaces from the shared common libraries, which are also used by WCF service. As such, we avoid the duplicate code of the service interfaces and the possible data contracts triggered by the service reference of the proxy way; also avoid the need to update service reference once in a while to synchronize the changes in the WCF service side. Certainly, the service reference of the proxy way can also use the shared data contracts in a common library, but a duplicate service interface and the occasional service reference updating are still unavoidable for the proxy way.
Visual Studio 2010 Projects in The Sample Application
Fold Structure and its Matching with Layers
Now, let us see what we have here in Visual Studio 2010 projects. We try to make the source code folder structure clear and simple; below is the folder structure for the whole application in solution explorer of Visual Studio:Diagram 2: Folder Structure in Visual Studio Solution Explorer for the Sample Application.
Our above folder structure matches the layers in Diagram 1 in following ways:
Client layer: The html pages shown in web browsers in user’s computers; these html pages are generated by client presenter layer project GH.Northwind/Clients/GH.Northwind.Web from the web server. We only have one type of client in this application so far.
Client presenter layer: all 2 projects under subfolder GH.Northwind/Clients.
Business layer: all 4 projects under subfolder GH.Northwind/Business.
Persistence layer: all 3 projects under subfolder GH.Northwind/Persistence.
Data layer: MS SQL server 2008 R2
Summary of The Main Folders and Projects
1) Project GH.Common: hold all common components which can be reused in any application, not just Northwind; it contains subfolders: LogService, ServiceLocator, Framework and etc. LogService is currently holding a log interface with a default implementation; you can plug in any type of log provider easily, such as log4Net, NLog, Enterprise Library log, the built-in Trace/Debug in .NET and etc. ServiceLocator implements a simple service locator by ninject package. Framework holds top level classes which are related to the architecture of all applications.2) Subfolder GH.Northwind: hold all projects related to Northwind application only. If there is a new application for class registration, we can add a new folder GH.ClassRegistration as a peer folder of GH.Northwind to hold all projects related to class registration application.
Under folder GH.Northwind, we have three subfolders:
Subfolder Business: Hold 4 projects related to business layer:- GH.Northwind.Business.Entities: a library project which holds all business POCO entities to be used in the whole Northwind application; business validations are currently also put in this folder.
- GH.Northwind.Business.Interfaces: a library project which holds all business operational interfaces to be used by client layer.
- GH.Northwind.Business: a core WCF business service lib project which implements the whole business operations and logic defined in project GH.Northwind.Business.Interfaces.
- GH.Northwind.Business.Host: a WCF service-host application project which mainly serves as a deployment purpose for project GH.Northwind.Business. Currently it is configured as a web service deployment.
- GH.Northwind.EntityFramework: a lib project to hold an Entity Framework.
- GH.Northwind.EntityFramework.Host: a WCF data service project which uses Entity Framework from project GH.Northwind.EntityFramework.
- GH.Northwind.Persistence: a regular class lib as a bridge between Business layer and Entity Framework.
Elaboration of Several Main Component Groups and Projects
1) Folder Framework in project GH.Common: holds all top level framework classes which drive each layer; these top level framework can be used in many applications, not just GH.Northwind. As such, we have an architecture with thin framework but fat applications. There are three subfolders further in this folder:a) Subfolder Persistence: an abstract adapter layer between the business layer and the Entity framework.
IPersistence
is a top level generic interface which only contains the basic database CRUD operations for demo and doesn't include some advanced database features, such as database many to many and many to one relationship handling. IPersistence
decouples the business layer and the actual persistence technique (such as Entity Framework); it also facilitates and simplifies the usage of persistence techniques in business layer. Below is this interface:
Hide Copy Code
public interface IPersistence<T>
{
void Insert(T entity, bool commit);
void Update(T entity, bool commit);
void Delete(T entity, bool commit);
void Commit();
IQueryable<T> SearchBy(Expression<Func<T, bool>> predicate);
IQueryable<T> GetAll();
}
PersistSvr
is a static class solely for the user-friendly usage purpose of the interface IPersitence
;PersistSvr
gets its PersistenceProvider
by service locator. Therefore, shifting to another Persistence provider is as easy as registering a different persistence provider by service locator. There are three subfolders in GH.Common.Framework.Persistence: DataServiceContext for WCF data service, ObjectContext
for default Entity Framework and DbContext
for Entity Framework Code first. All three are very similar. File PersistenceBase.cs in these folders implements interface IPersistence
by their own ways. Different people may implement this persistence layer differently. Our goal here is to minimize extra code in the concrete subclasses in project GH.Northwind.Persistence, which is described later. Below is the sample implementation for file PersistenceBase.cs in subfolder DbCxt
for DbContext
:
Hide Shrink Copy Code
public class PersistenceBase<T> : IPersistence<T> where T : BusinessEntityBase
{
protected String _entitySetName = String.Empty;
public ILogger<PersistenceBase<T>> Logger { get; set; }
public static ILogger<PersistenceBase<T>> Log
{
get { return Log<PersistenceBase<T>>.LogProvider; }
}
public static DbContext DataContext
{
get { return DataCxt.Cxt; }
}
#region IPersistence<T> Members
public virtual void Insert(T entity, bool commit)
{
InsertObject(entity, commit);
}
public virtual void Update(T entity, bool commit)
{
UpdateObject(entity, commit);
}
public virtual void Delete(T entity, bool commit)
{
DeleteObject(entity, commit);
}
public virtual void Commit()
{
SaveChanges();
}
public Expression<Func<T, bool>> predicate { get; set; }
public virtual IQueryable<T> SearchBy(Expression<Func<T, bool>> predicate)
{
return EntitySet.Where(predicate);
}
public virtual IQueryable<T> GetAll()
{
return EntitySet;
}
#endregion
protected virtual T FindMatchedOne(T toBeMatched)
{
throw new ApplicationException("PersistenceBase.EntitySet: Shouldn't get here.");
}
protected virtual IQueryable<T> EntitySet
{
get { throw new ApplicationException("PersistenceBase.EntitySet: Shouldn't get here."); }
}
protected virtual String EntitySetName
{
get { throw new ApplicationException("PersistenceBase.EntitySetName: Shouldn't get here."); }
}
protected void InsertObject(T entity, bool commit)
{
DataContext.Entry(entity).State = EntityState.Added;
try
{
if (commit) SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
throw;
}
}
protected void UpdateObject(T entity, bool commit)
{
try
{
DbEntityEntry entry = DataContext.Entry(entity);
DataContext.Entry(entity).State = EntityState.Modified;
if (commit) SaveChanges();
}
catch (InvalidOperationException e)
// Usually the error getting here will have a message:
// "an object with the same key already exists in the
// ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key"
{
T t = FindMatchedOne(entity);
if(t==null) throw new ApplicationException("Entity doesn't exist in the repository");
try
{
DataContext.Entry(t).State = EntityState.Detached;
(EntitySet as DbSet<T>).Attach(entity);
DataContext.Entry(entity).State = EntityState.Modified;
if (commit) SaveChanges();
} catch(Exception exx)
{
//Roll back
DataContext.Entry(entity).State = EntityState.Detached;
(EntitySet as DbSet<T>).Attach(t);
Log.Error(exx);
throw;
}
}
catch (Exception ex)
{
Log.Error(ex);
throw;
}
}
protected void DeleteObject(T entity, bool commit)
{
T t = FindMatchedOne(entity);
(EntitySet as DbSet<T>).Remove(t);
try
{
if (commit) SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
throw;
}
}
protected void SaveChanges()
{
try
{
DataContext.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// Update original values from the database (Similar ClientWins in ObjectContext.Refresh)
var entry = ex.Entries.Single();
entry.OriginalValues.SetValues(entry.GetDatabaseValues());
DataContext.SaveChanges();
}
} // End of function
}
The above implementation is pretty straightforward. One thing to be mentioned here is the protected function UpdateObject
. In this function, we catch the exception "InvalidOperationException
", which is usually due to the fact that the parameter-passed-in entity isn't currently tracked by ObjectStateManager
of EntityFramework. Then we detach the currently-tracked one and attach the parameter-passed-in one. Another thing to be mentioned is function SaveChange; we handle the optimistic concurrency exception in this function by loading the new values from database. For ObjectContext
and WCF data service, we need to handle these issues in their own ways. b) Subfolder Business: holds superclasses for the business layer. Currently an abstract class BusinessEntityBase is in this folder to server as a superclass of all business entities. It implements three interfaces: :
IValidatableObject
, IDataErrorInfo
, INotifyPropertyChanged
. IValidatableObject
is for class-level business validation. IDataErrorInfo
and INotifyPropertyChanged
are used for WPF clients with MVVM pattern for user-input data validation and interactive data binding between ViewModel and View, see the Microsoft MSDN article Implementing the MVVM Pattern for details. Currently in our sample solution, there is only a ASP .NET client, so interfaces IDataErrorInfo
and INotifyPropertyChanged
aren’t actually used in our sample solution; we comment them out so far. c) Subfolder Client: should hold all top classes for the client side. Currently only holds client side command-related classes which haven’t been used in this sample project.
2) Project GH.Northwind.Persistence in Folder \GH.Northwind\Persistence: a concrete adapter layer between the Northwind business layer and the Northwind Entity framework. Contains subclasses for the persistence of Northwind application for three different cases: WCF Data service, ObjectContext and DbContext. Subclasses here are very simple and inherit from super class PersistenceBase in a subfolder of folder GH.Common.Framework.Persistence. Because of their simplicity, we put all business related classes in one file. See the file NorthwindPrst.cs in folder DbCxt:
Hide Shrink Copy Code
namespace GH.Northwind.Persistence.DbCxt
{
public class CustomerPrst : PersistenceBase<Customer>
{
protected override IQueryable<Customer> EntitySet
{
get { return (DataContext as NorthwindEntities).Customers; }
}
protected override String EntitySetName
{
get { return _entitySetName ?? (_entitySetName =
Util.GetMemberNameExtra((NorthwindEntities g) => g.Customers)); }
}
protected override Customer FindMatchedOne(Customer toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.CustomerID == toBeMatched.CustomerID); }
}
public class ProductPrst : PersistenceBase<Product>
{
protected override IQueryable<Product> EntitySet
{
get
{
predicate = p => p.ProductID == 1;
return (DataContext as NorthwindEntities).Products;
}
}
protected override String EntitySetName
{
get { return _entitySetName ?? (_entitySetName =
Util.GetMemberNameExtra((NorthwindEntities g) => g.Products)); }
}
protected override Product FindMatchedOne(Product toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.ProductID == toBeMatched.ProductID); }
}
public class OrderPrst : PersistenceBase<Order>
{
protected override IQueryable<Order> EntitySet
{
get { return (DataContext as NorthwindEntities).Orders; }
}
protected override String EntitySetName
{
get { return _entitySetName ?? (_entitySetName =
Util.GetMemberNameExtra((NorthwindEntities g) => g.Orders)); }
}
protected override Order FindMatchedOne(Order toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.OrderID == toBeMatched.OrderID); }
}
public class Order_DetailPrst : PersistenceBase<Order_Detail>
{
protected override IQueryable<Order_Detail> EntitySet
{
get { return (DataContext as NorthwindEntities).Order_Details; }
}
protected override String EntitySetName
{
get
{
return _entitySetName ??
(_entitySetName = Util.GetMemberNameExtra((NorthwindEntities g) => g.Order_Details));
}
}
protected override Order_Detail FindMatchedOne(Order_Detail toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.OrderID ==
toBeMatched.OrderID && o.ProductID == toBeMatched.ProductID); }
}
public class SupplierPrst : PersistenceBase<Supplier>
{
protected override IQueryable<Supplier> EntitySet
{
get { return (DataContext as NorthwindEntities).Suppliers; }
}
protected override String EntitySetName
{
get
{
return _entitySetName ??
(_entitySetName = Util.GetMemberNameExtra((NorthwindEntities g) => g.Suppliers));
}
}
protected override Supplier FindMatchedOne(Supplier toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.SupplierID == toBeMatched.SupplierID); }
}
public class CategoryPrst : PersistenceBase<Category>
{
protected override IQueryable<Category> EntitySet
{
get { return (DataContext as NorthwindEntities).Categories; }
}
protected override String EntitySetName
{
get
{
return _entitySetName ??
(_entitySetName = Util.GetMemberNameExtra((NorthwindEntities g) => g.Categories));
}
}
protected override Category FindMatchedOne(Category toBeMatched) {
return EntitySet.DefaultIfEmpty(null).First(o => o.CategoryID == toBeMatched.CategoryID); }
}
}
You can see we only need to override two properties and one function from the super class: properties EntitySet, EntitySetName and function FindMatchedOne. For the EntitySetName, we try to avoid hard coding the name but resolve it by Lambda expression. Why? First, hard code is error prone. Also error from hard code can not be detected in compiling time but until runtime. With the strong-type Lamda expression solution, errors due to changes or typos can be detected during compilation. function FunctionMatchedOne
is to find the matched record by the same identity of the passed-in parameter toBeMatched; the identity may be a composite key, such as OrderId and ProductId in table Order_Detail. For DataServiceContext, we try to use the non-proxy way to access the data service. The service reference proxy with namespace HelpOnly is a dummy solely for the purpose to get the EntitySetName by Lamda function. If you don’t like this dummy, you can remove and then hard code the EntitySetName but with some disadvantages mentioned in above paragraph.
3) Project GH.Northwind.EntityFramework in folder \GH.Northwind\Persistence: this is a wcf lib project and hold an entity framework over the database Northwind. Read here for how to add entity framework models by the Entity Data Model Wizard. We only select 6 tables to simplify our demo: Products, Customers, Orders, Order_Details, Categories and Supplier. Basically, create an empty WCF lib project, then add ADO .NET Entity Classes with the wizard mentioned above. The default entity classes from the Entity Framework wizard are a little heavy weight. Then we can further transform them into light-weight DbContext POCO entity classes by the DbContext generator with wcf support; we will have detailed steps for how to do this later. Finally, we need to copy and paste the T4 template for these entity classes into project GH.Northwind.Business.Entities for the usages of the whole application. Following are the steps to achieve those tasks:
Step 1: Click and open file GHNorthwindModels.edmx in project GH.Northwind.EntityFramework, then you can see some table diagram. Righ click the diagram area, then click Add Code Generation Item, then click Online template, then Select ADO.NET C# DbContext Generator With WCF Support, click “Add” and then “OK”, two files Model1.Context.tt and Model1.tt are generated. Rename it as NorthwindModels.Context.tt and NorthwindModels.tt, Click NorthwindModels.tt, you can see all models there. Copy NorthwindModels.tt and paste it into project GH.Northwind.Business.Entities.
Step 2: Change below line in file NorthwindModels.tt in project GH.Northwind.Business.Entities from:
Hide Copy Code
string inputFile = @"GHNorthwindModels.edmx";To:
Hide Copy Code
string inputFile = @"..\..\Persistence\\GH.Northwind.EntityFramework\\GHNorthwindModels.edmx";Now the pasted NorthwindModels.tt points to the right path of the entity framework file.
Step 3: Right click NorthwindModels.tt in project GH.Northwind.Business.Entities, click "Run Custom Tool", now click file NorthwindModels.tt, you can see several entity files are generated. Their namespace is different from the namespace of the entities generated in project GH.Northwind.EntityFramework. From now on, we will refer the entities from project GH.Northwind.Business.Entities in the whole application, even in project GH.Northwind.EntityFramework.
Step 4: In project GH.Northwind.EntityFramework, add line “using GH.Northwind.Business.Entities” into file NorthwindModels.Context.cs so that Entity Framework refers to the Entities from project GH.Northwind.Business.Entities. Every time after you re-run the code generator tool, The added line "using GH.Northwind.Business.Entities" will be wiped out. You need to add manually again. Keeping using one version will avoid entity conflicts in many other places.
Step 5: Delete the original file NorthwindModels.tt in project GH.Northwind.EntityFramework because we no longer need it.
Above steps are for DbContext. For ObjectContext, the steps are the same, but make sure to use the code generator "ADO.NET POCO Entity Generator With WCF Support".
4) Project GH.Northwind.EntityFramework.Host in folder \GH.Northwind\Persistence: this is a wcf service application project with a wcf data service over the Entity Framework from project GH.Northwind.EntityFramework. Read here for how to add wcf data service over an Entity Framework.
Make sure to add project GH.Northwind.EntityFramework into this project as a reference. Also, Copy below connection string setting from GH.Northwind.EntityFramework config file to GH.Northwind.EntityFramework.Host config file; otherwise, won’t work:
Hide Copy Code
<connectionStrings>
<add name="NorthwindEntities"
connectionString="metadata=res://*/GHNorthwindModels.csdl|res://*/GHNorthwindModels.ssdl|
res://*/GHNorthwindModels.msl;provider=System.Data.SqlClient;provider
connection string="data source=WHU-DT\SQLEXPRESS10;initial
catalog=Northwind;integrated security=True;multipleactiveresultsets=True;
App=EntityFramework"" providerName="System.Data.EntityClient" />
</connectionStrings>
Usually, WCF data service works smoothly with ObjectContext
from Entity Framework. Now we are using DbContext in our Entity Framework. WCF data service doesn’t work with DbContext
directly so far. However, with some extra modification, it works fine. Below is the modified NorthwindDataService.svc based on here:
Hide Copy Code
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class NorthwindDataService : DataService<ObjectContext>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
// Allow full access rights on all entity sets
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
// return more information in the error response message for troubleshooting
config.UseVerboseErrors = true;
}
protected override ObjectContext CreateDataSource()
{
var ctx = new NorthwindEntities();
var oContext = ctx.ObjectContext;
oContext.ContextOptions.ProxyCreationEnabled = false;
return oContext;
}
}
The trick part of above modification is to use DbContext.ObjectContext
because DbContext
is built on top of ObjectContext
. Also, the DbContext.ObjectContext.ContextOptions.ProxyCreationEnabled
is turned off because DbContext
’s entity classes are POCO; POCO needs proxy creation to be turned off for the working of WCF data service.5) Project GH.Northwind.Business.Entities, GH.Northwind.Business.Interfaces and GH.Northwind.Business:We put the entities and the interfaces into their separate projects so that they can be distributed individually without distributing the business implementation to the business service users. As described in above section, the entity classes in project GH.Northwind.Business.Entities are auto-generated by the DbContext generator with wcf support; the entity metadata is also auto-generated by a code generator which will be described later. The interface in GH.Northwind.Business.Interfaces is business centric, not data centric. However, the interface here can include some CRUD operations for some business entities if these CRUD operations are business oriented. Besides some CRUD operations, the business interface needs to include all business operations needed by client sides. For tables Products, Customers, Orders, Order_Details, Categories and Suppliers, we can predict their CRUD operations will be needed, but CRUD operations for Orders and Order_Details will need to be combined together.
Creating the CRUD interface for entities is cumbersome particularly if the number of entities is big. There should be existing code generators which can help us. Other than using a third party code generator, here we will take advantage of the Domain Service class feature in .NET framework to help us. First create a domain service class in Project GH.Northwind.EntityFramework, based on the existing Entity Framework models. Make sure to build the project GH.Northwind.EntityFramework first before add a domain service class; otherwise you won’t be able to see the model classes in the domain service class wizard. Read here for how to create a domain service class with wizard. After the wizard is done, one file is generated: DomainService1.cs. Next, we need to extract useful info from this file by automatic way in Visaul Studio. a) Open file DomainService1.cs, mouse focus on class name DomainService1, then right click, select refactor->Extract Interface, then check "Place in another file", Then select the functions you want to extract. Here we select the CRUD operations for tables Customers, Products, Orders, Categories and Suppliers, but not table Order_Detail. Now we have an interface file: IDomainService1.cs. Rename this interface as
INorthwindSvr
, also modify it by adding [ServiceContract]
and [OperationContract]
, update its namespace and move it to project GH.Northwind.Business.Interfaces, which will be used by both client and business layers.Finally, Make sure files DomainService1.cs and IDomainService1.cs are deleted from project GH.Northwind.EntityFramework. Then, we need to further modify and expand interface INorthwindSvr in project GH.Northwind.Business.Interfaces to meet the business need of this demo; its updated version is as below:
Hide Shrink Copy Code
[ServiceContract]
public interface INorthwindSvr
{
[OperationContract]
List<Customer> GetCustomers();
[OperationContract]
void InsertCustomer(Customer customer, bool commit);
[OperationContract]
void UpdateCustomer(Customer currentCustomer, bool commit);
[OperationContract]
void DeleteCustomer(String customerId, bool commit);
[OperationContract]
List<Order> GetOrders();
[OperationContract]
List<Order_Detail> GetOrderDetailForAnOrder(int orderId);
[OperationContract]
List<Order> GetOrderForACustomer(String customerId);
[OperationContract]
void CreateOrder(Order order, Order_Detail[] details);
[OperationContract]
void UpdateOrder(Order currentOrder, Order_Detail[] details, bool commit);
[OperationContract]
void DeleteOrder(int orderId, bool commit);
[OperationContract]
void DeleteAnOrderDetailFromAnOrder(int orderId, int orderDetailId, bool commit);
[OperationContract]
List<Product> GetProducts();
[OperationContract]
Product GetProductById(int id);
[OperationContract]
void InsertProduct(Product product, bool commit);
[OperationContract]
void UpdateProduct(Product currentProduct, bool commit);
[OperationContract]
void DeleteProduct(int productId, bool commit);
[OperationContract]
List<Category> GetProductCategories();
[OperationContract]
List<Supplier> GetSuppliers();
[OperationContract]
void Commit();
}
In project GH.Northwind.Business, the implementation class NorthwindSvr is pretty simple due to library project GH.Northwind.Persistence. In addition, there is a quick way to create the function stubs based on an interface: open file NorthwindSvr.cs in Visual Studio 2010 editor, right click INorthwindSvr in file NorthwindSvr.cs, then click Implement interface-> Implement interface, the interface function stubs are created automatically in file NorthwindSvr.cs. With the help of the static class PersistSvr in GH.Common.Framework, we can implement these function stubs quickly.We need to initialize the persistence provider for NorthwindSvr. We put the initialization into a static constructor of class NorthwindSvr so that it can be called in the first moment of the access of the business class NorthwindSvr. We also use a configuration parameter "UseWcfDataService" to specify whether or not to use WCF data service. If not, then
ObjectContext
or DbContext
will be used directly. Below is the initialization code in :
Hide Shrink Copy Code
static NorthwindSvr() {
string useWcfDataService = System.Configuration.ConfigurationManager.AppSettings["UseWcfDataService"];
if (useWcfDataService.ToLower() == "true")
{
string dataServiceUrl = ConfigurationSettings.AppSettings["WcfDataServiceUrl"];
DataServiceCxtFrameWkNamespace.DataCxt.Cxt = new DataServiceContext(new Uri(dataServiceUrl));
ServiceLocator<IPersistence<Customer>>.RegisterService<DataServiceCxtNamespace.CustomerPrst>();
ServiceLocator<IPersistence<Product>>.RegisterService<DataServiceCxtNamespace.ProductPrst>();
ServiceLocator<IPersistence<Order>>.RegisterService<DataServiceCxtNamespace.OrderPrst>();
ServiceLocator<IPersistence<Order_Detail>>.RegisterService<DataServiceCxtNamespace.Order_DetailPrst>();
}
else {
if (typeof(NorthwindEntities).IsSubclassOf(typeof(ObjectContext))) {
// Below are commented out since now NorthwindEntities is DbContext;
// Uncomment them out if NorthwindEntities is ObjectContext;
/*ObjectCxtFrameWkNamespace.DataCxt.Cxt = new NorthwindEntities();
ServiceLocator<IPersistence<Customer>>.RegisterService<ObjectCxtNamespace.CustomerPrst>();
ServiceLocator<IPersistence<Product>>.RegisterService<ObjectCxtNamespace.ProductPrst>();
ServiceLocator<IPersistence<Order>>.RegisterService<ObjectCxtNamespace.OrderPrst>();
ServiceLocator<IPersistence<Order_Detail>>.RegisterService<ObjectCxtNamespace.Order_DetailPrst>();*/
}
else if (typeof(NorthwindEntities).IsSubclassOf(typeof(DbContext)))
{
DbCxtFrameWkNamespace.DataCxt.Cxt = new NorthwindEntities();
ServiceLocator<IPersistence<Customer>>.RegisterService<DbCxtNamespace.CustomerPrst>();
ServiceLocator<IPersistence<Product>>.RegisterService<DbCxtNamespace.ProductPrst>();
ServiceLocator<IPersistence<Order>>.RegisterService<DbCxtNamespace.OrderPrst>();
ServiceLocator<IPersistence<Order_Detail>>.RegisterService<DbCxtNamespace.Order_DetailPrst>();
}
else {
throw new NotSupportedException("NorthwindSvr: static Constructor: " +
typeof(NorthwindEntities).ToString() + " isn't a supported type.");
}
}
}
Metadata class and interface IValidatableObject
are good ways to do data validation in Client presenter layer for both ASP.NET MVC and WPF. There are many ways and code generators to auto-generate these metadata files. We use the code generator here to auto generate them. Namespace is updated afterward; tagMaxLength
is replaced by StringLength
since MaxLength
isn’t in System.ComponentModel.Annotation
of .NET 4. Also, for each partial entity class, we manually add super class BusinessEntityBase
in Framework of project GH.Common. Then we copy all metadata files into subfolder MetaAndPartial in project GH.Northwind.Business.Entities. The super class BusinessEntityBase
implements interfaceIValidatableObject
. Usually, for single property validation, the annotation tag can take care of easily. If there are validation rules which cross several entity properties, then we can override this interface in the entity subclass. In ASP .NET MVC3 and the Entity Framework code first DbContext
, the metadata class validation tags and interface IValidatableObject can be configured to be automatically triggered for validation in runtime. Below is the sample class CustomerMeta:In this file, we override the interface
IValidableObject
’s function Validate for a validation involved with both Address and Phone.
Hide Shrink Copy Code
[MetadataType(typeof(CustomerMetadata))]
public partial class Customer : BusinessEntityBase
{
override public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Address == String.Empty && Phone == String.Empty)
{
yield return new ValidationResult("Address and phone cannot be empty in the same time.",
new[] { "Address", "Phone" });
}
else
{
yield break;
}
}
}
public partial class CustomerMetadata
{
[DisplayName("Customer")]
[Required]
[StringLength(5)]
public string CustomerID { get; set; }
[DisplayName("Company Name")]
[Required]
[StringLength(40)]
public string CompanyName { get; set; }
[DisplayName("Contact Name")]
[StringLength(30)]
public string ContactName { get; set; }
[DisplayName("Contact Title")]
[StringLength(30)]
public string ContactTitle { get; set; }
[DisplayName("Address")]
[StringLength(60)]
public string Address { get; set; }
[DisplayName("Phone")]
[StringLength(24)]
public string Phone { get; set; }
// More are omitted here
}
For error handling in this WCF business service layer, we implement IErrorHandler
behavior in project GH.Northwind.Business to catch all exceptions and convert them into FaultException
so that the service channel won’t be faulted out and can be used again after an exception occurs. Also, the FaultException
can be sent to the client side to help debugging.6) Project GH.Northwind.Business.Host: this is a wcf service application which only holds the deployment configuration; all of its operations come from project GH.Northwind.Business. Currently, we use
BasicHttpBinding
host for this WCF service. We can change it easily to other type host such as Windows Service. Or we can create a new windows service host WCf project to access GH.Northwind.Business. Also, we can add windows service host support into the existing host project to achieve a dual host-supporting project.7) Project GH.Northwind.Client.Common in folder \GH.Northwind\Clients: part of the client presenter layer, which holds all common components needed by all types of different clients. We should put all common code for all clients to this lib as much as we can to maximize the code reusability for all types of clients. So far, we didn't put many contents in this project since we concentrate on business and persistence layer mainly in this article. A boolean configuration parameter "N-Tier" is used to specify whether or not the client will use three tiers or two tiers. If N-Tier is true, then the client will call the business wcf service handled by project GH.Northwind.Business.Host; otherwise, it will call the GH.Northwind.Business directly.
A better way to implement the client presenter layer is to use Command pattern to get better encapsulation and mobility for all types of clients. We may explore this in the future. Here, for simplicity, we just use class
NorthwindHandler
to expose the business interface INorthwindSvr
to clients. We use the non-proxy way to access the wcf business service. Below is the simple content of class NorthwindHandler
:
Hide Copy Code
public static INorthwindSvr GetNorthwindService()
{
if (ConfigurationSettings.AppSettings["N-Tier"].ToLower() == "true")
{
string northwindBusinessUrl = ConfigurationSettings.AppSettings["NorthwindSvrBusinessUrl"];
BasicHttpBinding binding = new BasicHttpBinding();
binding.MaxReceivedMessageSize = 1000000;
binding.ReaderQuotas.MaxDepth = 200;
ChannelFactory<INorthwindSvr> channelFactory = new ChannelFactory<INorthwindSvr>(
binding,
new EndpointAddress(new Uri(northwindBusinessUrl)));
INorthwindSvr northwindSvr = channelFactory.CreateChannel();
return northwindSvr;
}
else
{
return new NorthwindSvr();
}
}
8) Project GH.Northwind.Web in folder \GH.Northwind\Clients: is one type of clients: a ASP .NET MVC3 application for demo purpose. This project belongs to client presenter layer, which will generate the html web pages as a web client. Currently, we only implement CRUD operations for entity Product in this ASP.NET MVC3 client. See Start function Start in Global.asax as below:
Hide Copy Code
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
NorthwindController.NorthwindSvr = NorthwindHandler.GetNorthwindService();
System.Web.Mvc.ModelBinders.Binders.Add(typeof(CustomerWithOrderModel), new CustomerWithOrderBinder());
System.Web.Mvc.ModelBinders.Binders.Add(typeof(AllProductsModel), new AllProductsBinder());
System.Web.Mvc.ModelBinders.Binders.Add(typeof(SuppliersCategoriesModel), new SuppliersCategoriesBinder());
}
In the code, We inject an INorthwindSvr
instance to Northwind
controller class. If we want to unit test the client, we can inject a testing implementation of INorthwindSvr
. We also add three customized model bindersCustomerWithOrderBinder
, AllProductsBinder
and SuppliersCategoriesBinder
to store and retrieve easily instances of their matched models from the HTTP session. Below is the controller class NorthwindController
:
Hide Shrink Copy Code
public class NorthwindController : Controller
{
private readonly ILogger<NorthwindController> Log = Log<NorthwindController>.LogProvider;
private static INorthwindSvr _northwindSvr;
public static INorthwindSvr NorthwindSvr
{
get
{
if (_northwindSvr == null)
throw new ApplicationException("NorthwindController.NorthwindSvr: NorthwindSvr is null.");
return _northwindSvr;
}
set { _northwindSvr = value; }
}
public ActionResult Index()
{
return View();
}
public ActionResult AllProducts(AllProductsModel allProductsModel)
{
try
{
allProductsModel.Products = NorthwindSvr.GetProducts();
return View(allProductsModel.Products);
}
catch (FaultException<BusinessServiceException> e)
{
Log.Error("Web.NorthwindController.AllProducts(...) Error from Business service layer: " + e.Message);
throw;
}
catch (FaultException ex)
{
Log.Error("Web.NorthwindController.AllProducts(...) Error from Business service layer: " + ex.Message);
throw;
}
catch (Exception exx)
{
Log.Fatal("Web.NorthwindController.AllProducts(...) Error: " + exx.Message);
throw;
}
}
//
// GET: /NorthwindSvr/Details/5
public ActionResult DetailsProduct(int? id, AllProductsModel allProductsModel, SuppliersCategoriesModel model)
{
Product p = null;
if (id != null) p = allProductsModel.ProductById(id.Value);
else
{
p = allProductsModel.ProductById((int)TempData["id"]);
}
if (p.Supplier == null) p.Supplier = model.SupplierList.Where(s => s.SupplierID == p.SupplierID).DefaultIfEmpty(null).First();
if (p.Category == null) p.Category = model.CategoryList.Where(c => c.CategoryID == p.CategoryID).DefaultIfEmpty(null).First();
return View(p);
}
//
// GET: /NorthwindSvr/Create
public ActionResult CreateProduct(SuppliersCategoriesModel model)
{
ViewBag.SuppliersCategoriesModel = model;
return View(new Product());
}
//
// POST: /NorthwindSvr/Create
[HttpPost]
public ActionResult CreateProduct(Product p, AllProductsModel allProductsModel)
{
NorthwindSvr.InsertProduct(p, true);
TempData["id"] = p.ProductID;
allProductsModel.Products.Insert(allProductsModel.Products.Count, p);
return RedirectToAction("DetailsProduct");
}
//
// GET: /NorthwindSvr/Edit/5
public ActionResult EditProduct(int id, AllProductsModel allProductsModel,
SuppliersCategoriesModel suppliersCategoriesModel)
{
Product p = allProductsModel.ProductById(id);
ViewBag.SuppliersCategoriesModel = suppliersCategoriesModel;
return View(p);
}
//
// POST: /NorthwindSvr/Edit/5
[HttpPost]
public ActionResult EditProduct(Product p, SuppliersCategoriesModel suppliersCategoriesModel)
{
NorthwindSvr.UpdateProduct(p, true);
ViewBag.SuppliersCategoriesModel = suppliersCategoriesModel;
return View(p);
}
//
// GET: /NorthwindSvr/Delete/5
public ActionResult DeleteProduct(int id)
{
Product p = NorthwindSvr.GetProductById(id);
return View(p);
}
//
// POST: /NorthwindSvr/Delete/5
[HttpPost]
public ActionResult DeleteProduct(int id, FormCollection collection)
{
NorthwindSvr.DeleteProduct(id, true);
return RedirectToAction("AllProducts");
}
}
Below are some webpage screen shots for the CRUD operations of Product:Diagram 3: List all products.
Diagram 4: Create a product.
Diagram 5: Edit a product.
Diagram 6: Details of a product.
Diagram 7: Delete a product.
Discussion
Possible Configuration Results and Analysis of N-Tier Architectures From Diagram 1
For the configuration results of above N-Tier architecture in Diagram 1, we can come up with the following possible combinations by updating two parameters “N-Tier” and “UseWcfDataService” in the application configuration files. (In the table below, components which aren’t in parentheses are called by its directly-top layer; the component in parenthesis will be used directly by the component without parentheses in the same layer):Case No. | Client | Client Presenter Layer | Business Layer | Persistence Layer | Database | Tier |
1 | HTML Webpage | ASP .NET (Common Lib) | WCF Host (WCF Lib) | Persistence Lib (Entity Framework Lib) | SQL Server | 4-Tier, a complete N-Tier |
2 | HTML Webpage | ASP .NET (Common Lib) | WCF Host (WCF Lib) | Persistence Lib (WCF Data Service, Entity Framework) | SQL Server | 5-Tier, a complete N-Tier |
3 | HTMP Webpage | ASP .NET (Common Lib) | WCF Lib | Persistence Lib (Entity Framework Lib) | SQL Server | 3-Tier, not a complete 3-Tier |
4 | HTMP Webpage | ASP .NET (Common Lib) | WCF Lib | Persistence Lib (WCF Data Service, Entity Framework Lib) | SQL Server | 4-Tier, not a complete N-Tier |
5 | WPF | WPF Client Lib (Common Lib) | WCF Host (WCF Lib) | Persistence Lib (Entity Framework Lib) | SQL Server | 3-Tier, a complete 3-Tier |
6 | WPF | WPF Client Lib (Common Lib) | WCF Host (WCF Lib) | Persistence Lib (WCF Data Service, Entity Framework Lib) | SQL Server | 4-Tier, a complete N-Tier |
7 | WPF | WPF Client Lib (Common Lib) | WCF Lib | Persistence Lib (WCF Data Service, Entity Framework Lib) | SQL Server | 3-Tier, not a complete 3-Tier |
8 | WPF | WPF Client Lib (Common Lib) | WCF Lib | Persistence Lib (Entity Framework Lib) | SQL Server | 2-Tier, not a N-Tier |
Among all cases above, business layer and persistence layer run in one process in most of the cases except the cases that WCF data service is used. If WCF data service is used as part of the persistence layer, it will run in an individual layer with Entity Framework Lib, but persistence lib will still run in the same process as business layer. From here, we can see that layer and tier may not exactly match each other.
We need to mention another very common special situation in table 1: for cases 3, 4, 7, the WCF lib in the business layer is called directly by the client presenter layer, therefore the client presenter layer and business layer can only run in one process. For these cases, we can still come out 3 or 4 tier results because the whole application can run in 3 or 4 computers. We think these are an incomplete N-Tier architecture because the client presenter layer and business layer cannot run in separate machines (tiers); a complete N-Tier architecture should be able to run the client presenter layer and business layer in separate machines (tiers). A complete N-Tier architecture has the best capability to handle the scalability, security and fault tolerance issues. The main disadvantage for this incomplete 3-tier architecture is that it loses all advantages of an individually-deployed business tier. Those advantages include different security enforcement on business tier, individual scalability of business tier, and a central business tier for all clients and etc. However, if all mentioned advantages of an individual business layer are insignificant for some cases, then we can go to this incomplete N-Tier architecture to gain performance and save cost. This incomplete N-Tier architecture does happen very often. As software engineers, we probably all experienced it. The sample N-Tier architecture in this article can switch between the incomplete N-tier architecture and the complete N-tier architecture easily by updating configuration file only.
From table 1 above, we can see our N-Tier architecture is very configuration flexible; any type of tier architecture can be achieved easily, thanks to our good layer design and implementation.
Additional Advantages of N-Tier Architecture Sample Besides the General Advantages
Besides the general advantages of the N-Tier architecture mentioned in our previous article, our N-Tier architecture sample has the following additional advantages too:- Unit-testable for each layer: we achieve this by using service locator and dependency Injection. The dependency between layers is based on interfaces, not by concrete classes, then we can plug our test implementation classes of these interfaces easily for testing.
- Flexible for deployment: we achieve this by putting the WCF implementation into a library and put the WCF host in a separate WCF application project. In our sample code, only two boolean parameters are needed in the configuration file to control whether or not we want to deploy it as N-Tier or 2-tier architecture; whether or not we want to use Entity Framework directly or use WCF data service on the top of Entity Framework.
- Development efficient: all entities are automatically generated by code generators with wcf tag support; also metadata classes together with annotation tags are auto-generated too. We even auto-generate the draft version of the business side CRUD operation interface by domain service feature in .NET 4. In addition, for the persistence layer, the main implementation is in the folder Framework/Persistence of project GH.Common, which makes the persistence implementation in project GH.Northwind.Persistence much simpler and easier.
- Good decoupling and good encapsulation for each layer. We use a persistence layer to adapter the business layer with the EntityFramework so that these two layers are decoupled as much as they can. By this way, the business layer is unit-testable.
- Validation logic are maintainable and flexible. We put all validation logic in one place: project GH.Northwind.Business.Entities so that we only need to implement and maintain one version of validation logic. All layers share one version of validation logic. We can call these validations easily in many layers, based on our practical needs. Currently, we call these validations in our ASP .NET MVC3 client presenter layer.
- Better maintainable, reusable, clearer and conflict-free code by avoiding duplicate code. Firstly, we avoid using service refrerence proxy to access WCF service. Service reference proxy can easily duplicates the same piece of code. Instead, we use the non-proxy “ChannleFactory” to access WCF service and WCF data service by reusing the service interface and data contracts from shared common libraries. Secondly, we put the auto-generated entity classes and their metadata classes into a separate project GH.Northwind.Business.Entities, and then maintain and use them throughout the whole application.
Separation of WCF Implementation from WCF Host
In the business layer, we separate the implementation lib from the deployment host; in the persistence layer. we separate Entity Framework lib from the WCF data service host. By doing so, we achieve the flexibility of deployment. For example, we can easily change the host project to another type of deployment without changing the lib project. Also, we can use the lib directly without an individual host.Purpose of Library Project GH.Northwind.Persistence
Business layer doesn’t call Entity Framework or WCF data service directly, but through library GH.Northwind.Persistence. Why? there are following advantage of doing so: a) achieves a better decoupling between business layer and Entity Framework or WCF data service, so different types of persistence techniques can be swapped easily without the need to change the business service layer b) makes business layer unit-testable: the business layer works on the persistence interface only, therefore, we can unit-test business layer easily by injecting a testable version of persistence layer without actually connecting to the database; we should avoid using database in unit-test as possible as we can because database is slow in term of the need of unit test. Unit test should be fast. c) facilitates and simplifies the usage of persistence techniques in business layer. By checking the code in project GH.Northwind.Business and project GH.Northwind.Persistence, you can see this easily.Why Don't We Use the Domain Service Class Feature for Business Layer?
We take advantage of the feature of domain service class to auto-generate our business interface. But, why don't we use the domain service class directly for business layer? There are several disadvantages of using domain service classes directly: a) messy and hard to use because it is not a regular WCF that people are familiar with. The auto-generated domain service classes are big classes full of a lot of functions and properties and aren’t user-friendly. b) Domain service class creates two tight couplings: tight coupling between business layer and persistence layer (Entity Framework), tight coupling between client layer and business layer. These tight couplings bring many issues for unit test, upgrading, maintenance and etc. Domain service class is mixed together by both business logic and persistence operations; client sides use the domain service class directly. The business layer doesn’t depend on persistence operation by interface; the client layer doesn’t depend on business layer by interface. As such, you will have no way to unit test the client layer individually without business layer involved; you will have no way to unit test the business layer individually without persistence layer involved. Also, because of these couplings, updating a layer will bring bigger side effects on other layers.Why Don't Client Sides Call WCF Data Service Directly but Through Business Layer?
WCF data service in persistence layer is used as an optional persistence choice. Client side doesn’t call this WCF data service directly, but through a business layer interfaceINorthwindSvr
. Why don’t client sides call WCF data service directly? Firstly, WCF data service is data centric mainly for CRUD persistence operations, but client sides need a business operation provider; INorthwindSvr
is business oriented. Secondly, in INorthwindSvr
, we can modify the existing CRUD function easily by adding validation, business logic, extra parameters and etc for business reasons; we can also add extra business operational functions. In a word, we can do anything which we want in INorthwindSvr
; we have a full control over INorthwindSvr
. However, all of these mentioned above are limited in WCF data service. For example, in INorthwindSvr
, we can remove functioninsertOrderDetail
, but add one which is more business oriented function: AddProductToOrder(Product p, Order o)
. You cannot achieve this simply by a pure WCF data service. In a word, in order to have fully-blown and controllable business operations for client sides of N-Tier architecture, we need an individual business layer for client side other than letting client sides call the pure data centric WCF data service directly.Business Entity Classes and Code Generators
Only one version of business entity poco classes in project GH.Northwind.Business.Entities is maintained and used through the whole application, why? Doing so make us achieve code consistency, conflict-free, minimum maintenance effort because of just one version of code without duplicates. We are really trying to avoid the case that each layer uses its own version of entity classes, which trigger a lot of issues.Code generators are used to generate our code as much as possible. For a small number of business entities and operations, it is fine to manually write code without code generators. However, when the number of business entities and operations gets big, code generators will save us tons of efforts with consistent and error-free code. Manual coding tends to be more inconsistent and error prone. In case we need a varied version of auto-generated classes for different purposes in different layers, we can use the partial class feature in C# and metadata feature in namespace
System.ComponentModel.DataAnnotations
to expand and decorate these auto-generated classes. For example, for the auto-generated business entity classes, in ASP .NET MVC client, we can use the metadata class feature for data validation of user’s inputs. We can also use partial class feature to add new attribute, new inheritance, new function and etc into our existing entity classes. Because they are stored in separate files from the original auto-generated entity classes, metadata classes and partial classes will be kept intact if the original entity classes are auto-generated again.Data Validation
For data validation, we use metadata classes and interfaceIValidatableObject
. For the simple validation of individual properties, metadata classes with tags in System.ComponentModel.DataAnnotations
can be used. For class-level validation crossing multiple properties, interface IValidatableObject
can be used. With partial class feature, we let all of the auto-generated entity classes inherit from class BusinessEntityBase
inGH.Common.Framework.Business
; class BusinessEntityBase
implements interface IValidatableObject
with a virtual function Validate. Any entity class with their own complex validation rules can override this virtual function.Which layer should trigger the validation logic? For the simple validation with metadata classes, it can be checked in client layer, client presenter layer, business layer or Entity Framework, depending on the situation. For the class-level validation with interface
IValidatableObject
, it can be checked in client presenter layer, business layer or Entity Framework, but rarely in client layer. Client side validation is more performance efficient; server side validation is more reliable. We can do validation in multiple layers to achieve both efficiency and reliability. For example, In ASP .NET MVC3, metadata class validation can be configured to be checked in the client web page by the auto-generated JavaScript jQuery validation controls. However, client side validation in web page can be bypassed easily and intentionally by hacking, so we need to do the same validation in server side either in client presenter layer, business layer or Entity Framework for reliability. Regardless of where the validation will be checked, we only implement and maintain one version of validation logic in project GH.Northwind.Business.Entities in our sample N-Tier application; all layers share one version of validation logic. ASP .NET MVC3 supports auto validation with metadata class and interface IValidableObject
; Entity Framework code first DbContext
also supports these validations together with Fluent API by settingDbContext.Configuration.ValidateOnSaveEnabled
to true; for the client with WPF MVVM pattern, these annotation attribute validation can also be used to validate user data inputs, read article Attributes-based Validation in a WPF MVVM Application for this. Currently in our sample project, ASP .NET MVC3 will do the validation checking with metadata classes and interface IValidableObject
.Some Configuration Results of the Sample N-Tier Architecture Application
The results of some of the combinations of configuration parameters from the project configuration files are listed in table 2 as below:Note: True for configuration parameter "N-Tier" in GH.Northwind.Web means the client side accesses the business layer by WCF application project GH.Northwind.Business.Host; otherwise, the client side accesses the GH.Northwind.Business library directly. True for configuration parameter "UseWcfDataService" means GH.Northwind.Persistence library accesses the Entity Framework by WCF data service project GH.Northwind.EntityFramework.Host; otherwise, GH.Northwind.Persistence library accesses GH.Northwind.EntityFramework library directly. In addition, only configuration file in executable projects will take effect; configuration file in libraries won’t take effect. So, make sure to update configuration files for executable projects.
Parameter "N-Tier" in GH.Northwind.Web | Parameter "UseWcfDataService" in GH.Northwind.Web | Parameter "UseWcfDataService" in GH.Northwind.Business.Host | Application’s component flow |
True (GH.Northwind.Business.Host will be called) | N/A (Now, parameter UseWcfDataService in project GH.Northwind.Business.Host will take effect) | True | Web pages ⇔ GH.Northwind.Web ⇔ GH.Northwind.Client.Common ⇔ GH.Northwind.Business.Host ⇔ GH.Northwind.Business GH.Northwind.EntityFramework.Host ⇔ GH.Northwind.EntityFramework (Complete N-Tier: 5 tier) |
True (GH.Northwind.Business.Host will be called) | N/A (Now, parameter UseWcfDataService in project GH.Northwind.Business.Host will take effect) | False | Web pages ⇔ GH.Northwind.Web ⇔ GH.Northwind.Client.Common ⇔ GH.Northwind.Business.Host ⇔ GH.Northwind.Business GH.Northwind.EntityFramework (Complete N-Tier: 4 tier) |
False (GH.Northwind.Business.Host won’t be called; but lib GH.Northwind.Business will be called directly) | True | N/A (Now, parameter UseWcfDataService in project GH.Northwind.Web will take effect since GH.Northwind.Business.Host won’t be called) | Web pages ⇔ GH.Northwind.Web ⇔ GH.Northwind.Client.Common ⇔ GH.Northwind.Business ⇔ GH.Northwind.EntityFramework.Host ⇔ GH.Northwind.EntityFramework (Incomplete N-Tier: 4 tier) |
False (GH.Northwind.Business.Host won’t be called; but lib GH.Northwind.Business will be called directly) | False | N/A (Now, parameter UseWcfDataService in project GH.Northwind.Web will take effect since GH.Northwind.Business.Host won’t be called) | Web pages ⇔ GH.Northwind.Web ⇔ GH.Northwind.Client.Common ⇔ GH.Northwind.Business ⇔ GH.Northwind.EntityFramework (Incomplete N-Tier: 3 tier) |
Usages of the Flexibility of Deployment of The Sample N-Tier Architecture
The flexibility of deployment of different tier architectures in our sample project can have following possible usages:- Speed up development process sometimes. For example, in the development stage, we can mainly use the incomplete 3-tier configuration shown in the last row of above table to speed up the development if one team handles all tiers; development is speeded up because we don’t need to launch the services every time for testing our application in the two tier configuration. In most case, if the library work fine, the wcf host project should work fine too. But this isn’t always true since WCF service or data service has its own restrictions and rules, which will add requirement on the library interface. Therefore, once in a while, we also need to test our application under a complete N-Tier configuration with the wcf host service.
- In the initial development stage, if we are unsure how the final product will be deployed or if the deployment requirement will change with the growth of the project or the number of users, the flexible deployment strategy from our sample project can be used to challenge any possible deployment requirement in the future by updating the configuration files only. For example, in the beginning, we may choose not to use WCF data service. However, with more and more users for the application, we feel it is very needed to alleviate the workload of the data access into a separate computer, or we have some other types of applications which need to share a central data CRUD operation service, then WCF data service will be an optional solution. In our way, we only need to set configuration parameter "UseWcfDataService" to true, then host this WCF data service in another computer.
- The deployment flexibility will be very helpful if we want to deploy the same application in different tier architectures in different scenarios or for different customers. For example, for the same application, one customer wants a complete N-Tier architecture for Internet usage, but another customer want an incomplete 3-tier architecture only for Intranet usage.
Conclusions
- A highly decoupled, unit-testable, deployment-flexible, implementation-efficient and validation-flexible N-Tier architecture in .NET is introduced. What we achieved here is to put some well-known nice tools and features in .NET together and come up with a workable solution.
- Following main things are achieved in our Sample N-Tier Application:
- A layer depend on another layer by interfaces only, not by concrete classes.
- Different tier architectures can be switched easily simply by updating two parameter’s values in configuration files. We achieve this by separating WCF implementation from WCF Host into different projects, and also by the non-proxy way of the client side to access WCF service and WCF data service.
- To use it in the whole application, we auto-generate and maintain only one version of entity classes with the T4 template in a lightweight library project GH.Northwind.Business.Entities other than the original Entity Framework project so that we can regenerate and reuse everywhere these auto-generated entity classes from a lightweight library other than a place cluttered with the heavyweight Entity Framework stuff.
- Business layer doesn’t call Entity Framework directly, but through Persistance lib. Doing so helps us to achieve the maximum decoupling between business layer and persistence layer and allows us to swap the persistence technologies easily without any side effect on the business layer. Also, it facilitates and simplifies the usages of Entity Framework in business layer. The sample application demonstrates that we can swap the persistence technologies between WCF data service and DbContext(or: ObjectContext) easily by just updating a parameter's value in a configuration file.
- For data validation in our sample application, we use the auto-generated metadata classes for the simple validation of the individual properties and interface
IValidatableObject
for class-level validation crossing multiple properties. Currently, we put all validation logic in one place: project GH.Northwind.Business.Entities so that we only need to implement and maintain one version of validation logic. If somehow for security reason in certain cases, we can move all validation logic into its own library project. All layers share this version of validation logic. We can call these validations easily in many layers, based on our practical needs. Currently, we call these validations in the html webpage client layer and the ASP .NET MVC3 client presenter layer. - For error handling in this WCF business service layer, we implement
IErrorHandler
behavior to catch all exceptions and convert them into FaultException so that the service channel won’t be faulted out and can be used again after an exception occurs. Also, theFaultException
can be sent to the client side for helping debugging. - Try to auto-generate code as much as possible: all entity classes with WCF tags and metadata classes with annotation tags are automatically generated by code generators. We even auto-generate the draft version of the business interface by domain service class wizard.
- We use the non-proxy way in client side to access WCF service and WCF data service. Doing so can allow us to use data contracts (mainly the auto-generated entity classes) and service interfaces from the shared common libraries to avoid the duplicate code and the need to update the service reference of the proxy way.
- Besides the general advantages of the N-Tier architecture in our previous article, our N-Tier architecture sample has the following additional advantages too:
- Unit-testable for each layer: we achieve this by using service locator and dependency Injection for layer usages. Each layer depends on another layer only by a general Interface, not by concrete classes. Therefore, in unit testing, we can inject our efficient testing implementation of the interface for testing.
- Flexible for deployment: we achieve this by the separation of service library projects from service host projects.
- Implementation efficiency. all entities with wcf tags and metadata classes with annotation tags are automatically generated by code generators. We even auto-generate the draft version of the business interface by domain service class.
- Good decoupling and good encapsulation for each layer. We use a persistence layer to adapter and decouple the business layer with the EntityFramework. By this way, the business layer is unit-testable.
- Validation logic are maintainable and flexible. We put all validation logic in one place: project GH.Northwind.Business.Entities so that we only need to implement and maintain one version of validation logic. All layers share one version of validation logic. We can call these validations easily in many layers, based on our practical needs. Currently, we call these validations in our ASP .NET MVC3 client presenter layer.
- Better maintainable, reusable and clearer code by avoiding duplicate code. Firstly we avoid using service proxy to access WCF service; instead, we use the non-proxy way to access WCF service to minimize duplicate code. Secondly, we put the auto-generated entity classes and their metadata validation classes into a separate lightweight project, maintain and use them throughout the whole application.
- If the client presenter layer and business layer cannot run in separate machines (tiers), but the number of tiers are still more than 2. We categorize this architecture as an incomplete N-Tier architecture; a complete N-tier architecture should be able to run the client presenter layer and business layer in separate machines (tiers). The main disadvantage for the incomplete N-Tier architecture is that it loses all advantages of an individually-deployed business tier. Those advantages include different security enforcement on business tier, individual scalability of business tier and a central business tier for all clients and etc.
- The subfolder framework under project GH.Common holds all top level framework classes which drive each layer; these top level framework can be used in many applications, not just GH.Northwind. As such, we have an architecture with thin framework but fat applications.
- WCF data service is pure CRUD-data-operation oriented; a business layer is a business-oriented facade layer. In order to have fully-blown and controllable business operations for client sides in N-Tier architecture, we need an individual business layer for client side other than letting client sides call the pure data centric WCF data service directly.
- Domain service class feature in .NET creates two tight couplings: tight coupling between business layer and persistence layer (Entity Framework), tight coupling between client layer and business layer. These tight couplings bring many issues for unit test, upgrading, maintenance and etc. Domain service class is mixed together by both business logic and persistence operations; client sides use the domain service class directly. The business layer doesn’t depend on persistence operation by interface; the client layer doesn’t depend on business layer by interface. Therefore, domain service class feature isn't a good choice for a decoupled business layer in N-Tier architecture.
- The flexibility of deployment of any tier architecture in our sample project can have following possible usages:
- Speed up development process sometimes because we can use the 2-Tier configuration for development, which doesn’t need to launch the services every time for testing application.
- In the initial development stage, if we are unsure how the final product will be deployed or if the deployment requirement will change with the growth of the project or the number of users, the flexible deployment strategy from our sample project can be used to challenge any possible deployment requirement in the future by updating the configuration files only.
- The deployment flexibility will be very helpful if we want to deploy the same application in different tier architectures in different scenarios or for different customers.
No comments:
Post a Comment