企业应用程序架构:在 .NET 中设计应用程序和服务 - 第三部分






4.87/5 (16投票s)
用于客户订单管理系统的业务逻辑(引擎)和服务合同(管理器)设计。
引言
- 第一部分 (分布式应用程序层及项目详情):了解分布式环境中的分层设计是什么,以及我们在实际实现应用程序时将如何命名。
- 第二部分(数据库和库设计):了解数据库设计以及如何实现与 Edmx 容器交互的库。
- 第三部分(引擎和管理器设计):了解如何实现包含核心业务逻辑的引擎,以及如何实现具有服务契约的实际 WCF 服务。以及如何使用测试客户端测试服务[本文]
- 第四部分(客户端实现):了解如何使用 MVVM 模式实现调用服务的实际客户端。
这是关于企业应用程序架构设计的系列文章中的第三篇。在前几篇文章中,我们讨论了什么是分布式应用程序层,以及如何设计用于与数据库交互的数据库和库。因此,在本文中,我将介绍引擎和管理器实现。附加解决方案中的项目名称将与架构图右侧提到的名称保持一致,该图在图中进行了说明,并在第一部分中进行了解释。
业务逻辑(引擎)
它应该是在应用程序中包含业务逻辑的层,并位于数据库层和表示层之间。在确定业务逻辑应该放在哪里时,代码的可维护性始终是一个重要考虑因素。显然,我们可以在数据库层本身编写业务逻辑。但我们不应该这样做。以下是我们不应该这样做的原因。
- 将业务逻辑固定在数据库层会使其与数据库的技术实现紧密耦合。更改表会迫使我们再次更改许多存储过程,从而导致大量额外的错误和测试。
- 通常,UI 依赖业务逻辑进行验证等操作。将这些内容放在数据库中会导致数据库和 UI 之间紧密耦合,或者在不同情况下,这两个之间会复制验证逻辑。
- 要让多个应用程序处理同一个数据库会变得很困难。正如我们所讨论的,任何客户端都可以访问我们的服务和数据库。对一个应用程序的更改会导致其他应用程序中断。这很快就会变成维护噩梦。所以它确实无法扩展。
- 如果您非常确定您的客户端永远不会更改其数据库后端,那么您可以将业务逻辑放在数据库层。否则,不要这样做。
- 您的公司是否维护不同的团队,例如应用程序层的开发人员和 DBA 类型?那么可以继续使用业务层。
- 您是否有计划在未来尽量减少项目维护资源?那么可以继续使用业务层。
- 您无法将软件出售给有自己数据库后端偏好的公司。
- 除了这一点,还应考虑性能、完整性以及允许多个应用程序使用同一 API 的能力。
- 应用程序调试困难。
- 项目流程难以理解。
- 单元测试难以编写。
同样,我们可以说出很多原因。您甚至可以在网上搜索关于为什么业务逻辑的内容,您会从架构师那里获得大量基于他们经验的分享。然而,我并没有说您不能使用存储过程和视图来处理业务逻辑。但我的观点是,在表和应用程序之间添加一个额外的层来编写业务逻辑,以解耦这两者是一个好主意。这样,您可以更改数据库而不更改外部接口,从而允许您独立地重构数据库。最后但并非最不重要的,一切都在架构师手中。他必须选择正确的架构来帮助他的开发人员顺利地驱动项目,而不会产生任何开销。让我们开始实现包含业务逻辑的引擎。
业务逻辑实现(引擎)
正如我们在第一部分解释的架构图中所讨论的,在本文中,我们将讨论并实现从底部算起的第三个级别(业务层)。请看下图。
在开始讨论实现之前,我想问一个问题。应用程序中的哪些代码可以被视为业务逻辑?几乎应用程序中的每一行都可以被视为业务逻辑:)。然而,我们应该决定哪些主要区域在未来可以根据未来的需求进行重构。将这些部分放入引擎中。
最后,在实现业务逻辑时还需要考虑一件事。正如我们在前面的部分所讨论的,引擎直接与库交互,管理器与引擎交互以向客户端提供数据。由于客户端对 EntityObject 没有概念,引擎需要返回通用的原始业务对象而不是 EntityObject
。所以让我们为客户服务实现通用的业务对象和转换器。
通用转换器和业务对象实现
我们需要一个用于转换器实现的接口,该接口应包含一个泛型声明的方法。是的,这可以通过 C# 中的 where
子句来实现。它用于指定泛型声明中定义的类型参数的类型约束。这是将在实际转换器类中继承的接口。
public interface IObjectConverter
{
/// <summary>
/// Converts the data object to entity type or entity to data object.
/// </summary>
TTarget Convert<tsource,>(TSource source, TTarget target)
where TSource : class
where TTarget : class;
}
这是将 CustomerEntityObject
转换为 Customer
对象的实际转换器代码。
public class CustomerEntityToCustomerDataConverter : IObjectConverter
{
#region IObjectConverter Members
public TTarget Convert<tsource,>(TSource source, TTarget target)
where TSource : class
where TTarget : class
{
var src = source as CustomerEntityObject;
var tgt = target as Customer;
if (src == null)
{
throw new Exception("Source should not be null");
}
// Cloning the object.
var custData = new Customer();
custData.Address1 = src.Address1;
custData.Address2 = src.Address2;
custData.City = src.City;
custData.Country = src.Country;
custData.CustomerID = src.CustomerID;
custData.Email = src.Email;
custData.FirstName = src.FirstName;
custData.Image = src.Image;
custData.LastName = src.LastName;
custData.Phone = src.Phone;
custData.State = src.State;
return custData as TTarget;
}
#endregion
}
这里,Customer
是数据合同类,它在通用项目中包含数据成员,供引擎和管理器使用。
[DataContract]
public class Customer
{
[DataMember]
public Int32 CustomerID
{
get
{
return _CustomerID;
}
set
{
_CustomerID = value;
}
}
private global::System.Int32 _CustomerID;
[DataMember]
public String FirstName
{
get
{
return _FirstName;
}
set
{
_FirstName = value;
}
}
private global::System.String _FirstName;
[DataMember]
public global::System.String LastName
{
get
{
return _LastName;
}
set
{
_LastName = value;
}
}
private global::System.String _LastName;
[DataMember]
public global::System.Byte[] Image
{
get
{
return _Image;
}
set
{
_Image = value;
}
}
private global::System.Byte[] _Image;
[DataMember]
public global::System.String Address1
{
get
{
return _Address1;
}
set
{
_Address1 = value;
}
}
private global::System.String _Address1;
[DataMember]
public global::System.String Address2
{
get
{
return _Address2;
}
set
{
_Address2 = value;
}
}
private global::System.String _Address2;
[DataMember]
public global::System.String City
{
get
{
return _City;
}
set
{
_City = value;
}
}
private global::System.String _City;
[DataMember]
public global::System.String State
{
get
{
return _State;
}
set
{
_State = value;
}
}
private global::System.String _State;
[DataMember]
public global::System.String Country
{
get
{
return _Country;
}
set
{
_Country = value;
}
}
private global::System.String _Country;
[DataMember]
public global::System.String Phone
{
get
{
return _Phone;
}
set
{
_Phone = value;
}
}
private global::System.String _Phone;
[DataMember]
public global::System.String Email
{
get
{
return _Email;
}
set
{
_Email = value;
}
}
private global::System.String _Email;
}
以下是解决方案树中的项目详细信息。
类似地,我们可以根据需要为 Order
、OrderDetail
和 Product EntityObject
实现其他转换器和业务对象。现在我们可以继续实现引擎。
客户服务引擎
让我们开始实现实际的服务引擎,它包含业务逻辑。声明 ICustomerServiceEngine
接口,供管理器与引擎交互。该接口包含两个方法声明。一个用于获取完整的客户列表,另一个用于使用客户 ID 获取特定客户的详细信息。
public interface ICustomerServiceEngine
{
List<customer> GetCustomers();
Customer GetCustomer(int customerID);
}
实现 CustomerServiceEngine
及其所需的属性和方法。在实现实际接口方法之前,我们需要引擎类中的库实例来获取实体对象上下文。
public class CustomerServiceEngine : ICustomerServiceEngine
{
#region ICustomerService Members
CustomerServiceLibrary customerServiceLibrary;
public CustomerServiceLibrary CustomerServiceLibrary
{
get
{
if (customerServiceLibrary == null)
{
customerServiceLibrary = new CustomerServiceLibrary();
}
return customerServiceLibrary;
}
}
CustomerServiceLibrary
有一个名为 GetEntitiesObjectContext
的方法,它返回实际的实体对象上下文。这是我们在第二部分已经讨论过的客户服务库类中的代码。
public class CustomerServiceLibrary : ICustomerServiceLibrary
{
Customer_DatabasesEntities context = null;
public CustomerServiceLibrary()
{
context = new Customer_DatabasesEntities();
}
public Customer_DatabasesEntities GetEntitiesObjectContext()
{
return context;
}
...
}
在 CustomerServiceEngine
类中,GetCustomers
方法使用实体对象上下文从数据库返回所有客户详细信息。
public List<customer> GetCustomers()
{
List<customer> customerList = new List<customer>();
CustomerEntityToCustomerDataConverter convertEntityToData =
new CustomerEntityToCustomerDataConverter();
Customer convertedItem = null;
foreach (CustomerEntityObject custData in CustomerServiceLibrary.GetCustomers
(CustomerServiceLibrary.GetEntitiesObjectContext()))
{
convertedItem = convertEntityToData.Convert
<customerentityobject,>(custData, null);
customerList.Add(convertedItem);
}
return customerList;
}
在这里,您可以看到我们正在返回带有称为 Customer
的原始业务对象的客户列表。此方法使用 libraries.GetCustomers
方法获取客户详细信息,并使用我们在通用项目中实现的转换器将每个实体对象转换为业务对象。
接下来开始实现 GetCustomer
方法,该方法根据客户 ID 返回单个客户的详细信息。
public Customer GetCustomer(int customerID)
{
var FilteredCustList = from customer in GetCustomers()
where customer.CustomerID == customerID
select customer;
return FilteredCustList.FirstOrDefault();
}
就这样。我们完成了 CustomerServiceEngine
的实现。最后,客户服务引擎项目在解决方案中的外观如下,已准备好发布。
让我们开始在 WCF 中实现 ServiceManager
,它有助于为客户端提供服务 API。
服务管理器实现(管理器)
由于我们将通过 WCF 技术实现管理器,您可能会问为什么要选择 WCF。因为,基于 Windows Communication Foundation (WCF) 平台的服务的灵活性很高。WCF 的前提是开发人员可以编写一次服务,然后通过更改配置文件将其部署为不同的终结点。实质上,WCF 就是关于如何配置您的服务,但操作配置文件涉及许多开发人员容易忽略的细节。例如,当大型团队中的开发人员更改配置文件时,会出现意外错误。随着团队熟悉配置文件中的模式,他们的能力会提高。
如果您是初学者,并且对 WCF 没有基本了解,请参考这篇文章 文章。
第一步是创建 Windows Communication Foundation 服务契约。这基本上是一个标记为显示它是服务契约的接口。在开始实际服务实现之前,让我们详细介绍如何创建 WCF 项目和配置文件。
作为初步步骤,创建一个新的项目,类型为 **WCF 服务库**,并将其命名为 ServiceManagers.CustomerServiceManager
。项目创建完成后,将 IService.cs 重命名为 ICustomerServiceManager.cs,将 Service1.cs 重命名为 CustomerServiceManager.cs。并删除其中的所有现有代码。
将 Common.DataContracts
和 ServiceEngines.CustomerServiceEngine
项目添加为项目引用。现在打开 ICustomerServiceManager.cs 文件以声明所需的服务契约,并在其中添加以下服务契约代码。
namespace ServiceManagers
{
[ServiceContract]
public interface ICustomerServiceManager
{
[OperationContract]
List<customer> GetCustomers();
[OperationContract]
Customer GetCustomer(int customerID);
}
}
接下来,实现服务。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using ServiceEngines;
using System.Collections.ObjectModel;
namespace ServiceManagers
{
public class CustomerServiceManager : ICustomerServiceManager
{
public CustomerServiceManager()
{
}
ICustomerServiceEngine customerServiceEngine = null;
public ICustomerServiceEngine CustomerServiceEngine
{
get
{
if (customerServiceEngine == null)
{
customerServiceEngine = new CustomerServiceEngine();
}
return customerServiceEngine;
}
}
#region ICustomerServiceManager Members
public List<common.datacontracts.customer> GetCustomers()
{
return CustomerServiceEngine.GetCustomers();
}
public Common.DataContracts.Customer GetCustomer(int customerID)
{
return CustomerServiceEngine.GetCustomer(customerID);
}
#endregion
}
}
在上面的代码中,只需根据服务管理器契约调用客户服务引擎方法。例如,GetCustomers
契约调用客户服务引擎 GetCustomers()
方法,该方法根据业务逻辑返回实际数据。在此级别,我们已完成服务契约的实现。
让我们从 config 文件的最顶部开始。打开 app.config 文件,您会在 config 文件中的 <services />
节点下看到服务部分。服务需要一个名为 name
的元素,该元素必须是实现接口的类的完全限定类名。
<services>
<service name="ServiceManagers.CustomerServiceManager"></service>
</services>
为了从外部访问服务,您需要指定一个终结点。此终结点将具有以下地址
<endpoint contract="ServiceManagers.ICustomerServiceManager" binding="basicHttpBinding"
address="https:///ServiceManagers/CustomerServiceManager
" />
终结点至少需要地址、绑定和契约。Address
是可以调用服务的位置。现在该服务公开了一个名为 ICustomerServiceManager
的 Contract
。这就是使服务可用的全部内容。在客户端,您所需要做的就是共享契约,您应该能够使用该服务。让我们部署服务。
准备好客户服务管理器以供发布
如您所知,我们为了清晰起见,在 WCF 服务库中创建了服务契约。我们无法直接将这些服务库部署到 IIS。因此,创建一个部署 Web 项目来部署我们所有的服务管理器库,并使其准备好发布。
使用 **Visual Studio IDE 中的 ASP.NET 网站模板** 创建一个新项目,并将其命名为 Web.ServiceManagers
。
项目创建完成后,如果您愿意,请删除所有不需要的文件。例如,删除以下文件,并使项目保持非常简单,只有 web.config 文件和 app_data 文件夹。
右键单击 Web 项目,然后选择“添加新项”以创建 .svc 文件,以便为客户端提供服务。您可以创建简单的文本文件并将其命名为 CustomerServiceManager.svc,或者像下图所示的那样,通过 Visual Studio 中的项目项模板创建 WCF 服务。
如果您按照上图所示创建 .svc 文件作为 WCF 服务,请删除添加此 .svc 时自动创建的所有 .cs 文件,因为我们已经在要通过项目引用的服务管理器项目库中创建了所有服务契约。
现在打开 .svc 文件,并在 Service
属性中添加带有正确服务名称的标签。
<%@ ServiceHost Language="C#" Debug="true"
Service="ServiceManagers.CustomerServiceManager" %>
接下来,详细介绍 web.config 配置文件。打开 web.config 文件,删除所有现有内容,并粘贴以下代码。
<?xml version="1.0"?>
<configuration>
<!—Connection String should be mentioned here -->
<connectionStrings>
<add name="Customer_DatabasesEntities" connectionString="metadata="res://*/Data
Contracts.CustomerOrderManagementModel.csdl|
res://*/Data Contracts.CustomerOrderManagementModel.ssdl|res://*/Data
Contracts.CustomerOrderManagementModel.msl";provider=System.Data.SqlClient;
provider connection string='Data Source=IN09PC77-PC\SQLEXPRESS;Initial
Catalog="Customer Databases";Integrated
Security=True;MultipleActiveResultSets=True'"
providerName="System.Data.EntityClient"/>
</connectionStrings>
<system.serviceModel>
<services>
<service name="ServiceManagers.CustomerServiceManager"
behaviorConfiguration="metadataAndDebug">
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
<endpoint address="" binding="basicHttpBinding"
bindingConfiguration=""
contract="ServiceManagers.ICustomerServiceManager" />
<host>
<baseAddresses>
<add baseAddress="https:///CustomerServiceManager/" />
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="metadataAndDebug">
<serviceMetadata httpGetEnabled="true" httpGetUrl=""/>
<serviceDebug httpHelpPageEnabled="true"
includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.web>
<compilation debug="true" targetFramework="4.0">
</compilation>
<pages controlRenderingCompatibilityVersion="3.5"
clientIDMode="AutoID"/></system.web>
</configuration>
此文件包含与我们在服务管理器项目中已讨论过的 app.config 文件类似设置,除了连接字符串信息。ConnectionString
元素通过服务器信息帮助连接到 SQL Server。可以在 config 文件的顶部包含此信息,以便于理解。您可以从 edmx 项目中获取项目的连接字符串。
<connectionStrings>
<add name="Customer_DatabasesEntities" connectionString="metadata="res://*/Data
Contracts.CustomerOrderManagementModel.csdl|res://*/Data
Contracts.CustomerOrderManagementModel.ssdl|res://*/Data
Contracts.CustomerOrderManagementModel.msl";provider=System.Data.SqlClient;
provider connection string='Data Source=IN09PC77-PC\SQLEXPRESS;
Initial Catalog="Customer Databases";
Integrated Security=True;MultipleActiveResultSets=True'"
providerName="System.Data.EntityClient"/>
</connectionStrings>
此文件中的其余终结点和服务名称配置与我们在服务管理器库中已讨论过的 app.config 文件相似。如果愿意,您可以将其复制并粘贴到此处。架构设计到此为止。现在我们有了**数据库、与数据库交互的库、包含业务逻辑的引擎以及公开服务契约供客户端使用的管理器**。现在,让我们开始讨论如何部署我们的服务,并确保我们的 CustomerServiceManager
服务已上线。有许多方法可以测试我们的服务。但我们将直接通过 Visual Studio 进行测试。将我们的 Web 项目设置为 Visual Studio 中的 StartUpProject
并运行应用程序。您会看到 Internet Explorer 显示的文件列表如下。
单击 CustomerServiceManager.svc 文件,您会看到 Internet Explorer 显示 CustomerServiceManager
服务已就绪。
您仍然可以使用页面顶部的 svcutil 调用您的服务。点击该链接了解您的服务是否运行正常。
让我们将 Web 项目部署到 IIS,以便直接从 Web 浏览器或客户端调用我们的服务。以下是将服务部署到 IIS 的步骤。
步骤 1
生成解决方案,并确保所有内容都能成功编译。
第二步
复制 Web 项目的物理路径。只需浏览项目并复制物理位置。这是我机器上的 Web 项目物理位置 - C:\Mine\Samples\Enterprise Sample\Code Project\CustomerOrderManagementSystem\Web.ServiceManagers。
步骤 3
通过在 Windows 的运行对话框中键入 inetmgr
来打开 IIS Manager。
步骤 4
浏览站点并转到“默认网站”。在此右键单击并选择“添加应用程序”。
步骤 5
将别名命名为 CustomerOrderManagementSystem
,并通过单击“选择”按钮在应用程序池中选择 ASP.NET v4.0。
粘贴您的物理路径并单击“确定”以保存设置。就这样!我们已经部署了服务 Web 项目,我们的服务已准备好发布。
现在,您可以使用以下 URL 在本地计算机上访问客户服务管理器。
最后,我们完成了服务器端实现,下一部分将介绍客户端实现。在下一部分,我们将讨论 COMS 中的其他服务实现,以及**设计 Silverlight 客户端时的 MVVM 方法**。不过,我附带了一个简单的测试客户端来运行演示应用程序。只需下载源代码并运行即可从数据库显示数据(如果您有)。您可以参考第二部分或另一篇关于如何构建数据库的文章以获取更多关于如何构建数据库的想法。
坦白说,我仍然有很多信息可以在本文中详细解释。但本文的内容已经很多了。如果我把所有信息都放在本文中,您会觉得文章太长,需要更多时间阅读:)。所以,我只关注在这个系列中我想为读者传达的内容。显然,我们可以从互联网上的其他文章中获取有关特定领域的更多详细信息。
注意
如果在运行演示应用程序时遇到任何问题,请参考此提示/技巧文章来解决问题。如果您仍然有任何问题,请告诉我。我会尽快提供解决方案。
希望您已经了解了如何设计分层架构及其用途。如果您有任何疑问,请发布评论。当然,请不要忘记投票:)。
历史
- 2010 年 11 月 15 日:初始帖子