通用多用途.NET分层框架






4.83/5 (27投票s)
一个通用的多用途分层框架示例,用于简化现代.NET应用程序开发
引言
我从.NET框架早期就开始.NET开发(可以追溯到2002年的1.0版本)。虽然那时像关注点分离(SOC)、通过接口实现松耦合、面向接口编程而非面向实现以及通过依赖注入(DI)实现控制反转(IOC)等概念还不是热门话题(那时我们主要忙于对抗“DataSet”),但我已经开始考虑将逻辑分离成不同的(逻辑)层。尽管如今关于这个概念的信息很多,但要找到一个完善的示例并不容易。因此,我决定自己创建一个。希望您喜欢……
我关于分层框架的设想拓扑结构
拟议的拓扑结构包含五个主要的逻辑层。这些是检索、验证和存储数据的常用层。这是大多数现代框架的通用方法。接下来,我区分了五个所谓的“共享层”,它们为一个或多个主要层提供功能。我将在接下来的章节中简要讨论这五个主要层以及它们对“共享层”的依赖关系。
Visual Studio中的项目解决方案结构
Visual Studio项目解决方案结构大部分反映了前面描述的拟议拓扑。一个重要的注意事项是,由于所有层都通过接口耦合,因此所有层都可以根据需要被自定义实现替换。在本教程中,我创建了一个代码优先的Entity Framework数据模型作为我们的数据存储层。在解决方案中,该层的实现可以在EntityFrameworkDbLayerCodeFirst项目中找到。接下来,数据访问层在EntityFrameworkDataAccessLayer和EntityFrameworkGenericRepositoryLayer中实现。数据服务层由VS解决方案中的DataServiceLayer项目实现。该层负责从数据访问层读取数据,准备检索到的数据以供表示层使用,并将数据回传给数据访问层。然后,业务逻辑层由VS解决方案中的BuinessLogicLayer表示。该层充当数据访问层和表示层之间的中介。最后,表示层取决于最终用户的特定需求,可能具有多种形式(Web应用程序、Winforms应用程序、移动应用程序……)。对于本文,我创建了一个简单的控制台应用程序、一个Windows Forms应用程序(它将通过引用WCF Web服务来使用我们服务层的特殊变体)以及一个ASP.NET MVC 5应用程序(由Z*类型项目表示),它将直接与业务逻辑交互。我将在接下来的章节中逐一介绍每个层(我也会在相应的章节解释我没有特别提及的特定“辅助”层,例如FaultLayer、UtilityLayer、ModelLayer、EnumLayer、DataTransfertObjectLayer和WcfServiceLayer)。我将高层次地解释数据存储、数据服务和业务逻辑层,并在解释最后一层(表示层)时更深入地探讨每个层(否则我将不得不重复相同的内容两次)。当然,完整的细节可以在附件的代码库中找到。
数据存储
数据存储由EntityFrameworkDBLayerCodeFirst项目实现。如上所述,我创建了一个简单的域模型,其中包含电力或燃气设备的存储。然后,我从域模型创建了一个实体框架上下文(DeviceContext)。DeviceContext类用作域类和依赖的SQL Server数据库之间的ORM(对象关系映射器)。
数据库上下文
namespace EntityFrameworkDBLayerCodeFirst { public partial class DeviceContext : DbContext { public DbSet<Device> Devices { get; set; } public DbSet<ElecDevice> ElecDevices { get; set; } public DbSet<GasDevice> GasDevices { get; set; } public DeviceContext() : base("DeviceContext") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); } } }
领域模型
为了传播可重用性,模型本身存在于一个单独的项目ModelLayer中。在本篇文章中,我创建了一个非常简单的模型,其中包含一个抽象基类、一个包含通用设备蓝图的抽象类以及两个具体的设备类,分别代表电力或燃气设备。
namespace ModelLayer { public abstract class BaseModel { public string EntityName { get; set; } public bool IsDirty { get; set; } } public abstract partial class Device : BaseModel { public int DeviceId { get; set; } public string DeviceName { get; set; } public string SerialNumber { get; set; } public string ImageUrl { get; set; } } public partial class ElecDevice : Device { public bool IsSmartElecDevice { get; set; } } public partial class GasDevice : Device { public bool IsSmartGasDevice { get; set; } } }
域模型类图和数据库图
上图显示了我们简单域模型的类图。
上图显示了由实体框架从我们的域模型创建的数据库模型。正如您将注意到的,EF创建了一个单一表来表示我们底层SQL Server数据库中的类继承层次结构。尽管我们在域层中创建了一个基类、一个抽象设备类和两个具体的设备类(一个用于电力,一个用于燃气),但我们只有一个数据库表。这被称为“表继承”(TPH),是EF的默认设置。(或者,您可以配置EF以创建“表类型”(TPT)或“具体类表”(TPC))。一个鉴别器列存储了不同具体类型之间的区别。因此,在我们的例子中,Discriminator字段将根据创建的类型保存“ElecDevice”或“GasDevice”值(这些反映了具体模型类的名称)。
数据访问
数据访问层负责数据存储层存储的数据库数据的检索和持久化。数据访问层还负责将域模型对象(它们是持久化感知的)转换为所谓的DTO或简单的数据传输对象。将(在我们的例子中)DbSet特定的表暴露给上层是一个普遍的好习惯,以避免技术特定的代码传递到业务逻辑和表示层。DTO对象只包含“数据”(具有getter和setter的属性),没有持久化特定的代码,没有验证方法,也没有业务逻辑,换句话说,只传输POCO(纯粹的CLR对象)。再次,……思考SOC(关注点分离)。此处涉及的解决方案类包括EntityFrameworkDataAccessLayer
、ModelLayer
、DataTransferObjectLayer
、DataserviceLayer
和EntityFrameworkGenericRepositoryLayer
。我将在接下来的章节中简要介绍它们。
工作单元和存储库模式
在ASP.NET MVC控制器方法中结合工作单元和存储库模式的示例。
尽管我们的域模型(为演示目的)只包含一个表,但我们仍然应该以事务性的方式实现我们的数据持久化。这意味着如果我们持久化一个或多个表上的多个插入、更新、删除操作,这些操作应该正确地提交到底层数据库。为实现此目的,所提出的拓扑结构使用UnitOfWork模式结合基于Repository的数据检索和持久化模型。下图展示了我们如何在概念上实现这一点。有关实现细节,请参阅代码库类:InterfaceLayer.IUnitOfWork
、InterfaceLayer.IGenericRepository
、EntityFrameworkGenericRepositoryLayer.EntityFrameworkGenericRepository
和EntityFrameworkDataAccessLayer.UnitOfWork
。
数据服务
DataServiceLayer是DataAccessLayer和BusinessLogicLayer之间的中介。数据服务层使用数据访问层检索数据并将其转换为DTO对象。然后,业务逻辑层使用DTO对象作为表示层和数据验证的基础。接下来,在表示层中对各自的DTO数据进行操作并在业务逻辑层中进行验证后,数据服务负责将DTO映射回表示层所代表的持久化感知的表示(由模型层表示)。所提出的解决方案使用AutoMapper来实现对象之间的映射。不同类之间的关系如下图所示。有关实现细节,请参阅代码库类:InterfaceLayer.IUnitOfWork, InterfaceLayer.IGenericRepository
、EntityFrameworkGenericRepositoryLayer.EntityFrameworkGenericRepository
、EntityFrameworkDataAccessLayer.UnitOfWork
。和DataServiceLayer.ElecDeviceDataService
业务逻辑
BusinessLogicLayer是DataServiceLayer和PresentationLayer之间的中介。业务逻辑层负责将DTO对象发送到表示层,并在持久化之前验证DTO中包含的数据。有关实现细节,请参阅代码库类:InterfaceLayer.IValidationDictionary
、InterfaceLayer.IElecDeviceDataService
、UtilityLayer.ModelStateWrapper
、BusinessLogicLayer.BaseBusinessLogic
和BusinessLogicLayer.ElecDeviceLogic
。
服务逻辑
如果我们想将我们的业务暴露给外部世界(这意味着.NET环境之外),那么我们应该在业务逻辑类周围构建一个“包装器”。在提出的解决方案中,这是通过使用基于WebService(WCF)的方法实现的。有关实现细节,请参阅代码库类:WcfServiceLayer.IElecDeviceWcfService
、WcfServiceLayer.ElecDeviceWcfService
、InterfaceLayer.IBusinessLogic
、BusinessLogicLayer.ElecDeviceLogic
和FaultLayer.GenericFault
。
表示逻辑
为了快速展示框架的使用,我添加了两个小型应用程序。第一个是基于ASP.NET MVC5的应用程序,它通过依赖注入(使用Ninject)直接使用业务逻辑层。第二个是一个Windows Forms应用程序,它通过Web服务引用间接引用业务逻辑。我将在本文的其余部分简要解释这两种方法。
处理电力设备管理的ASP.NET MVC 5应用程序
为此,我创建了一个默认的ASP.NET MVC 5应用程序。最重要的类如上所示在项目结构中突出显示。首先,我们有NinjectWebCommon.cs类,我们将使用它来注入我们业务逻辑类所需的依赖项。接下来,我们有MVC控制器ElecDevicesController,它持有我们业务逻辑组件的引用。我们控制器中的每个CRUD(创建、读取、更新、删除)操作都有其在ElecDevices子文件夹中定义的视图表示。最后,我们的MVC应用程序包含一个Web.config文件,其中保存了我们数据存储层的连接字符串。
NinjectWebCommon.cs
/// <summary> /// Load your modules or register your services here! /// </summary> /// <param name="kernel">The kernel.</param> private static void RegisterServices(IKernel kernel) { kernel.Bind<IUnitOfWork<DeviceContext>>().To<UnitOfWork>(); kernel.Bind<IElecDeviceDataService>().To<ElecDeviceDataService>(); }
我使用Ninject(请参见:http://www.ninject.org/)作为依赖注入引擎。Ninject是一个文档齐全且轻量级的DI容器,用于解析您所有应用程序中的接口依赖项。对于我们简单的Web应用程序,我们只需要将ElecDataService
和UnitOfWork
实现类注入到我们的MVC控制器中。这通过在NinjectWebCommon.cs类中的RegisterServices方法中添加适当的绑定来实现。
表示层:ElecDevicesController.cs
该控制器包含我们ElecDevice模型的CRUD(创建-读取-更新-删除)操作。为简洁起见,我将只解释构造函数代码、读取和更新。插入和删除操作类似,可以从代码库中查看。
构造函数
IElecDeviceDataService _elecDeviceDataService; ElecDeviceLogic _elecDeviceLogic; public ElecDevicesController(IElecDeviceDataService elecDeviceDataService) { _elecDeviceDataService = elecDeviceDataService; _elecDeviceLogic = new ElecDeviceLogic(_elecDeviceDataService, new ModelStateWrapper(this.ModelState)); }
控制器构造函数获取数据服务实现类注入。接下来,控制器持有对业务逻辑类的引用(拥有)。最后,使用注入的数据服务类和模型状态实例化业务逻辑类以进行验证。
读取数据
// GET: ElecDevices public ActionResult Index() { IList<ElecDeviceDTO> elecDeviceDTOList = _elecDeviceLogic.GetListCollection(); if (_elecDeviceLogic.HasErrors) { return RedirectToAction("ShowError", "Error", new { error = _elecDeviceLogic.ErrorMessage }); } return View(elecDeviceDTOList.ToList<ElecDeviceDTO>()); }
我们MVC控制器类的Index操作负责从数据源检索设备(在我们的情况下,是所有设备……)。为了实现这一点,我们只需在我们的业务逻辑类上执行GetListCollection()
方法。另请注意,我们的表示层控制器不知道数据是如何检索的,这符合关注点分离(SOC)模式的良好应用。最后,检索到数据后,将其传递给相关的视图。
更新数据
// GET: ElecDevices/Edit/id public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } ElecDeviceDTO elecDeviceDTO = _elecDeviceLogic.GetEntityById(id); if (elecDeviceDTO == null) { return HttpNotFound(); } if (_elecDeviceLogic.HasErrors) { return RedirectToAction("ShowError", "Error", new { error = _elecDeviceLogic.ErrorMessage }); } return View(elecDeviceDTO); } // POST: ElecDevices/Edit/id [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "DeviceId,DeviceName,SerialNumber,ImageUrl,IsSmartElecDevice")] ElecDeviceDTO elecDeviceDTO) { _elecDeviceLogic.Update(elecDeviceDTO); if (ModelState.IsValid) { return RedirectToAction("Index"); } return View(elecDeviceDTO); }
更新数据涉及两个步骤。第一步是“GET”Edit操作,我们加载选定的项目(GetListXXX方法返回所有数据列表,当我们接下来要编辑一个项目时,我们应该检索特定选定项目的详细信息)。另请注意,业务逻辑层返回的是DTO类型。然后将返回的数据传递给视图操作。第二步是“POST”Edit操作,它将可能的更新值从视图中获取。这两个操作都需要我们的Business Logic实例,第一个用于检索当前项目,第二个用于持久化可能修改过的数据。同样,我们的控制器也不知道用于检索或持久化数据的底层技术。
验证数据
在我们的控制器构造函数中:_elecDeviceLogic = new ElecDeviceLogic(_elecDeviceDataService, new ModelStateWrapper(this.ModelState));
我们将模型状态提供给了我们的业务逻辑。ModelState保存了绑定到视图的数据。正如您稍后将看到的,验证逻辑已添加到业务逻辑层。如果提供的数据违反了验证规则,则会返回适当的错误消息并在表示层中显示。
业务逻辑层:ElecDeviceLogic.cs
构造函数
public partial class ElecDeviceLogic : BaseBusinessLogic, IBusinessLogic<ElecDeviceDTO> { private IElecDeviceDataService _elecDeviceService; public ElecDeviceLogic(IElecDeviceDataService elecDeviceService) { _elecDeviceService = elecDeviceService; ValidationDictionary = null; } public ElecDeviceLogic(IElecDeviceDataService elecDeviceService, IValidationDictionary validationDictionary) { _elecDeviceService = elecDeviceService; ValidationDictionary = validationDictionary; } ... }
此示例项目公开了一个非常简单的域对象(ElecDevice),因此为简单起见,我们提供了一个仅映射到此单个域对象的业务逻辑层。事实上,关于如何定义业务逻辑层,“硬规则”并不存在。我们可以像这个例子一样,在域对象、DTO和业务逻辑类之间建立一对一映射,或者我们可以将多个域对象集中到一个业务逻辑类中,所有这些都取决于我们希望如何在应用程序中公开数据。例如,我们可能有一个单一的业务逻辑类来代表一个完整的订单跟踪系统,例如,一个客户域类、一个订单头类和一个订单行类,每个都映射到一个特定的实体域类(客户、订单、订单明细),但作为单个DTO公开,并包装在一个单一的业务逻辑类中……请注意,我们的业务逻辑类有两个构造函数,每个都注入了数据服务类,第二个构造函数接受一个验证对象作为参数。
读取数据
public IList<ElecDeviceDTO> GetListCollection(Expression<<, bool>> filter = null, string includeProperties = "") { this.Initialize(); try { List<ElecDeviceDTO> elecDeviceDTOList = _elecDeviceService.GetElecDevices(); InfoMessage = "List of ElecDeviceDTO retrieved !"; return elecDeviceDTOList; } catch(Exception ex) { BuildErrorMessage(ex); return null; } }
我们业务逻辑类中的GetListCollection
方法持有对注入的数据服务类的引用。反过来,数据服务类将访问底层存储层以检索数据。虽然在此示例中未使用,但GetListCollection
提供了一个过滤器表达式,可用于限制要返回的数据量。
更新数据
public void Update(ElecDeviceDTO entityToUpdate) { this.Initialize(); try { if (!this.Validate(entityToUpdate)) { Exception ex = new Exception("Update of existing device did not pass business logic validation, please check validationerrors for more info !"); ex.Source = string.Format("{0}.{1}", this.GetType().AssemblyQualifiedName, this.GetType().Name); BuildErrorMessage(ex); } else { _elecDeviceService.UpdateElecDevice(entityToUpdate); InfoMessage = String.Format("Update Succeeded:\r\n{0}", ObjectMetaData<ElecDeviceDTO>.GetObjectState(entityToUpdate, null)); } } catch (Exception ex) { BuildErrorMessage(ex); } } public bool Validate(ElecDeviceDTO entityToValidate) { if (String.IsNullOrEmpty(entityToValidate.DeviceName)) ValidationDictionary.AddError("DeviceName", "The name of the device is a required field !"); if (!String.IsNullOrEmpty(entityToValidate.ImageUrl)) { if (!entityToValidate.ImageUrl.Trim().StartsWith("~") || entityToValidate.ImageUrl == null) ValidationDictionary.AddError("ImageUrl", "The URL of the image must start with a relative path notation (~) !"); } return ValidationDictionary == null ? true: ValidationDictionary.IsValid; }
在由数据服务层处理持久化之前,业务逻辑层首先检查任何验证规则。请注意,业务逻辑层不抛出任何错误,而是构建一个自定义错误消息,其中包含所有验证规则的违规以及/或所有其他可能遇到的错误。然后,由表示层负责检查HasErrors
标志并采取适当行动。
数据服务层:ElecDeviceService.cs
构造函数
public partial class ElecDeviceDataService : IElecDeviceDataService { IUnitOfWork<DeviceContext> _repositoryService; public ElecDeviceDataService(IUnitOfWork<DeviceContext> repositoryService) { _repositoryService = repositoryService; Mapper.CreateMap<ElecDevice, ElecDeviceDTO>(); Mapper.CreateMap<ElecDeviceDTO, ElecDevice>(); } ... }
数据服务的构造函数持有对存储库服务的注入引用。在我们的例子中,存储库服务是UnitOfWork的实现,它持有对EntityFramework上下文类的引用,并负责实际的CRUD操作。当然,由于我们是针对接口开发的,因此我们可以轻松地用替代的实现替换数据层(或提供一个“模拟”类用于测试目的……)。数据服务层还负责持久化感知数据对象(在我们的例子中是EntityFramework DbSet实体)与数据传输对象(DTO)之间的映射。
读取数据
public List<ElecDeviceDTO> GetElecDevices() { List<ElecDevice> elecDeviceList = _repositoryService.ElecDeviceRepository.GetListCollection().ToList<ElecDevice>(); List<ElecDeviceDTO> elecDeviceDTOList = Mapper.Map<List<ElecDevice>, List<ElecDeviceDTO>>(elecDeviceList); return elecDeviceDTOList; }
GetElecDevices
方法从存储库服务读取数据。由于返回的结果是EF实体,因此必须先将它们转换为DTO实体,然后才能返回给业务逻辑层。
更新数据
public void UpdateElecDevice(ElecDeviceDTO elecDeviceToUpdateDTO) { ElecDevice elecDeviceToUpdate = Mapper.Map<ElecDeviceDTO, ElecDevice>(elecDeviceToUpdateDTO); _repositoryService.ElecDeviceRepository.Update(elecDeviceToUpdate,elecDeviceToUpdate.DeviceId); _repositoryService.Commit(); }
UpdateElecDevice
方法从业务逻辑层接收一个(已验证的)DTO对象,将其映射到持久化感知(EF实体)对象,并将更新委托给相应的存储库服务。
数据访问层:工作单元和存储库
存储库和工作单元模式旨在为应用程序的数据访问层和业务逻辑层创建抽象层。实现这些模式可以帮助您的应用程序不受数据存储变化的影响,并有助于自动化单元测试或测试驱动开发(TDD)。
存储库:EntityFrameworkGenericRepository.cs
public partial class EntityFrameworkGenericRepository<TEntity, TDBContext> : IGenericRepository<TEntity, TDBContext> where TEntity : class where TDBContext : DbContext { internal TDBContext context; internal DbSet<TEntity> dbSet; public EntityFrameworkGenericRepository(TDBContext context) { this.context = context; this.dbSet = context.Set<TEntity>(); } ... }
每个实体类(由泛型占位符TEntity表示)将成为数据库上下文(由泛型占位符TDBContext表示)的一部分。当我们在下一节讨论工作单元模式时,这种方法会更清晰,但现在,只需记住我们使用一个泛型类(由IGenericRepository接口确定)来持久化我们的实体。该泛型类包含了对每个由TEntity表示的实体类都有用的所有通用CRUD操作。通过创建泛型类,我们可以避免为上下文中的每个特定实体一遍又一遍地重写这些样板代码。接下来,我们将详细介绍我们通用数据持久化类的GetListCollection
和Update
方法。(请参阅代码库以获取其余方法)。
public IList<TEntity> GetListCollection(Expression<Func<TEntity, bool>> filter = null, string includeProperties = "") { IList<TEntity>query = dbSet.ToList<TEntity>(); if (filter != null) { throw new NotImplementedException(); } foreach (var includeProperty in includeProperties.Split (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { throw new NotImplementedException(); } return query; } ... }
GetListCollection
从底层上下文检索所需的实体(可能带有过滤器,此处超出范围……)。TEntity将被替换为其真实世界的实现(例如,在我们的例子中是ElecDevice)。
public virtual void Update(TEntity entityToUpdate, Object entityId) { TEntity originalEntity = dbSet.Find(entityId); context.Entry(originalEntity).State = EntityState.Detached; dbSet.Attach(entityToUpdate); context.Entry(entityToUpdate).State = EntityState.Modified; }
update方法接受两个参数,第一个参数是TEntity
类型,包含更改的值,第二个参数是修改实体的PK(主键)。update方法首先从数据库提取原始实体并将其从上下文中分离出来。然后将修改后的版本附加到上下文中,并将状态设置为已修改。当父数据服务调用commit语句时,更改将在底层数据库中更新。
工作单元:UnitOfWork.cs
public partial class UnitOfWork : IUnitOfWork<DeviceContext> { private bool _disposed = false; private DeviceContext _context; private EntityFrameworkGenericRepository<ElecDevice, DeviceContext> _elecDeviceRepository; public IGenericRepository<ElecDevice, DeviceContext> ElecDeviceRepository { get { if (_elecDeviceRepository == null) { _elecDeviceRepository = new EntityFrameworkGenericRepository<ElecDevice, DeviceContext>(_context); } return _elecDeviceRepository; } } public UnitOfWork() { _context = new DeviceContext(); } public void Commit() { _context.SaveChanges(); } ... }
UnitOfWork模式是我们的存储库模式的包装器。在我们的演示中,我们只对单个实体(ElecDevice)应用CRUD操作,但在实际应用程序中,应该处理许多不同类型的实体,并且如果它们应该以事务方式处理(这意味着所有更新作为一个整体通过或失败),那么它们应该共享相同的上下文。例如,如果我们还需要处理GasDevice
类型,我们可以在UnitOf Work
类中添加一个_gasdeviceRepository
属性,它共享相同的上下文。下面的示例代码演示了这一点。
private EntityFrameworkGenericRepository<GasDevice, DeviceContext>_gasDeviceRepository; public IGenericRepository<GasDevice, DeviceContext> GasDeviceRepository { get { if (_gasDeviceRepository == null) { _gasDeviceRepository = new EntityFrameworkGenericRepository<GasDevice, DeviceContext>(_context); } return _gasDeviceRepository; } }
处理电力设备管理的Windows Forms应用程序
ASP.NET MVC应用程序直接引用了业务逻辑层。在第二个演示中,我们希望将我们的业务逻辑公开为服务,以便非.NET客户端也可以访问该逻辑。为了演示这种方法,我创建了一个简单的Windows Forms项目,用户可以在其中检索、更新、插入和删除电力设备。
private void FormDevice_Load(object sender, EventArgs e) { try { this.Cursor = Cursors.WaitCursor; _wcfProxy = new ServiceReferenceElecDevice.ElecDeviceWcfServiceClient(); } catch(Exception ex) { this.ResetBindingSource(); MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { this.Cursor = Cursors.Default; } }
在添加了WCF服务引用后,我们首先创建一个WCF服务代理实例。
private void getDeviceButton_Click(object sender, EventArgs e) { try { this.Cursor = Cursors.WaitCursor; Int32 searchId; Int32.TryParse(deviceIdTextBox.Text, out searchId); this.elecDeviceDTOBindingSource.DataSource = _wcfProxy.GetElecDeviceById(searchId); } catch(FaultException<ServiceReferenceElecDevice.GenericFault> faultEx) { MessageBox.Show(ErrorHandler.GetError(faultEx) + "\r\n" + faultEx.Detail.ErrorDetails,"Error Occured",MessageBoxButtons.OK,MessageBoxIcon.Error); } catch (Exception ex) { this.ResetBindingSource(); MessageBox.Show(ErrorHandler.GetError(ex),"Error Occured",MessageBoxButtons.OK,MessageBoxIcon.Error); } finally { this.Cursor = Cursors.Default; } }
当用户请求设备时,代理方法将被执行并返回设备作为DTO。返回的DTO绑定到Windows窗体数据源的绑定源。任何异常(故障等)都会被捕获并显示在消息框中。
private void updateButton_Click(object sender, EventArgs e) { try { this.Cursor = Cursors.WaitCursor; elecDeviceDTOBindingSource.EndEdit(); _elecDeviceDTO = (elecDeviceDTOBindingSource.Current as ElecDeviceDTO); if(_elecDeviceDTO != null) { _wcfProxy.UpdateElecDevice(_elecDeviceDTO); } } catch (FaultException<ServiceReferenceElecDevice.GenericFault> faultEx) { MessageBox.Show(ErrorHandler.GetError(faultEx) + "\r\n" + faultEx.Detail.ErrorDetails, "Error Occured", MessageBoxButtons.OK, MessageBoxIcon.Error); } catch (Exception ex) { this.ResetBindingSource(); MessageBox.Show(ErrorHandler.GetError(ex), "Error Occured", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { this.Cursor = Cursors.Default; } }
更新过程类似(删除和插入也是如此),在这种情况下,代理会执行UpdateElecDevice
方法,并且任何错误(故障等)都会被捕获并显示在消息框中。接下来,我将解释由服务托管的代码。
服务层:WcfServiceLayer.cs
服务接口
[ServiceContract] public interface IElecDeviceWcfService { [FaultContract(typeof(GenericFault))] [OperationContract] ElecDeviceDTO GetElecDeviceById(int? id); [FaultContract(typeof(GenericFault))] [OperationContract] IList<ElecDeviceDTO> GetElecDevices(); [FaultContract(typeof(GenericFault))] [OperationContract] void InsertElecDevice(ElecDeviceDTO entityToInsert); [FaultContract(typeof(GenericFault))] [OperationContract] void UpdateElecDevice(ElecDeviceDTO entityToUpdate); [FaultContract(typeof(GenericFault))] [OperationContract] void DeleteElecDevice(int? id); }
如前所述,我们的服务层将是业务逻辑层的“包装器”。为此,我们首先定义一个WCF服务接口,它公开了消费客户端(例如我们的Windows应用程序)可以在服务上执行的方法。在我们的例子中,我们公开了典型的CRUD方法来读取、插入、更新和删除电力设备。另外请注意,每个公开的方法都包含一个FaultException
。如前所述,故障异常是一类特殊的异常,专门用于Web服务交互。这是因为消费客户端可能不总是.NET客户端。故障异常作为标准的SOAP消息返回,因此也可以被非.NET消费者解释。
服务实现
与其他实现一样,我们将解释限制在构造函数、读取和更新操作。有关服务提供的所有其他方法,请参阅代码库。
public class ElecDeviceWcfService : IElecDeviceWcfService { IUnitOfWork<DeviceContext> _unitOfWork; IElecDeviceDataService _elecDeviceDataService; IBusinessLogic<ElecDeviceDTO> _elecDeviceLogic; public ElecDeviceWcfService() { _unitOfWork = new UnitOfWork(); _elecDeviceDataService = new ElecDeviceDataService(_unitOfWork); _elecDeviceLogic = new ElecDeviceLogic(_elecDeviceDataService, new ModelStateWrapper(new Dictionary())); } public ElecDeviceWcfService(IBusinessLogic<ElecDeviceDTO> elecDeviceLogic) { _elecDeviceLogic = elecDeviceLogic; } ... }
请注意,我们的WCF服务有两个构造函数。默认的无参数构造函数,以及第二个接收“注入”(通过DI容器)的设备逻辑的构造函数。默认情况下,生成的WCF代理只公开一个默认的无参数构造函数,因此默认情况下不支持任何形式的依赖注入。(代理是公开服务契约表示的单个CLR接口的CLR类。代理提供了与服务契约相同的操作,但还具有管理代理生命周期和与服务连接的附加方法。代理完全封装了服务的各个方面:它的位置、它的实现技术和运行时平台以及通信传输)。尽管如此,您可以更改WCF代理的生成方式,从而为它们提供接受参数的构造函数。但为了简单起见,我们将坚持使用无参数构造函数。最重要的一点是,我们的WCF服务持有对我们业务逻辑类的引用,因此可以执行我们业务逻辑类公开的公共方法。另请注意,我们的业务逻辑组件的实例化添加了默认ModelStateWrapper
的实例,这将为我们的服务添加验证。
public ElecDeviceDTO GetElecDeviceById(int? id) { try { Int32 newId; bool converted = Int32.TryParse(id.ToString(), out newId); if (!converted) { this.ThrowFault( "Unable to parse the id parameter to an integer value !", "Please provide a valid integer number as id !"); } ElecDeviceDTO returnValue = _elecDeviceLogic.GetEntityById(newId); if (_elecDeviceLogic.HasErrors) { this.ThrowFault( string.Format("An error occured while executing {0}.{1}", _elecDeviceLogic.GetType().AssemblyQualifiedName, this.GetType().Name), _elecDeviceLogic.ErrorMessage); } return returnValue; } catch(FaultException<GenericFault> genericFault) { throw genericFault; } catch (Exception ex) { this.ThrowFault(ErrorHandler.GetErrorMessage(ex),ErrorHandler.GetErrorDetails(ex)); } return null; }
GetElecDeviceById
接收一个整数ID作为输入(要搜索的设备ID),检查是否提供了有效的ID,否则抛出故障错误。然后,服务在业务逻辑实例上执行GetEntityById
,并在找到时返回DTO。如果未找到或发生任何错误,则服务将适当的故障异常返回给消费客户端。
public void UpdateElecDevice(ElecDeviceDTO entityToUpdate) { try { _elecDeviceLogic.Update(entityToUpdate); if (_elecDeviceLogic.HasErrors) { this.ThrowFault( string.Format("An error occured while executing {0}.{1}", _elecDeviceLogic.GetType().AssemblyQualifiedName, this.GetType().Name), _elecDeviceLogic.ErrorMessage); } } catch (FaultException<GenericFault> genericFault) { throw genericFault; } catch (Exception ex) { this.ThrowFault(ErrorHandler.GetErrorMessage(ex), ErrorHandler.GetErrorDetails(ex)); } }
更新方法将要更新的实体委托给业务逻辑组件。如果在更新过程中发生任何错误(验证或其他),则Web服务将向消费客户端抛出故障异常。
结语
在当今现代(.NET)应用程序开发中,充分理解如何将应用程序构建成分层框架至关重要。我从经验中得知,许多开发人员急于直接编写代码,快速获得结果,但这往往会导致糟糕的架构和过多的对某种技术的依赖。这听起来可能像陈词滥调,但在软件开发中,唯一不变的是“变化”,每个软件架构师都应该努力从一个适合变化的开发平台开始。诸如面向接口编程而非面向实现、依赖注入以及通过松耦合的逻辑层实现关注点分离(SOC)等术语是良好软件开发的核心要素。请随意使用和改编代码以适应您的项目。
不使用代码
为了能够上传项目,已删除了所有依赖项(包、bin、obj……),因此需要解析依赖项(通过Nuget自动解决,或在必要时手动解决)。