65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WCF 和 Entity Framework 改进数据访问 Web 服务

starIconstarIconstarIconstarIconstarIcon

5.00/5 (15投票s)

2013 年 11 月 21 日

CPOL

15分钟阅读

viewsIcon

49484

downloadIcon

1435

一篇带有示例应用程序的文章, 展示了如何以及可以做什么来改进和增强 WCF 和 EF 数据访问 Web 服务

引言

基于 SOAP 的 Web 服务是封装多层业务数据应用程序(尤其是在企业环境中)的数据访问层(DAL)和业务逻辑层(BLL)的可靠、可扩展且安全的方式。对此数据访问架构的负面论点主要集中在性能和复杂性上。随着 WCF 和 Entity Framework 的进步,这些缺点已经减小。另一方面,对结构和代码的改进和增强可以进一步提高服务的性能和可维护性,并降低复杂性。本文将首先介绍如何设置示例应用程序,然后详细介绍以下重点主题。

设置示例应用程序

可以在 SQL Server 2012 Express 或 LocalDB 中创建 StoreDB 示例数据库,然后使用 Visual Studio 或 SQL Server Management Studio (SSMS) 执行包含的脚本文件 StoreDB.sql 来填充表。您可以在此处下载 SQL Server Express with LocalDB 和 SSMS。

WCF Web 服务设置为 basicHttpBinding 模式,并托管在 ASP.NET 网站上。采用数据库优先方法的 EF 数据模型设置在类库项目中。您可以直接使用 .NET Framework 4.0 的 Visual Studio 2010 或 .NET Framework 4.5 的 Visual Studio 2012/2013 轻松编译下载的源代码。除 .NET Framework 外,所有引用的程序集都包含在示例应用程序源代码根目录下的 Referenced.Lib 文件夹中。Visual Studio 解决方案中的项目显示在下面的截图中。

如果您不使用 SQL Server 2012 LocalDB,则需要在 SM.Store.Service.web.config 文件中将 connectionStringdata source 值 "(localdb)\v11.0" 更改为您自己的 SQL Server 实例。

<add name="StoreDataEntities" 
connectionString="metadata=res://*/StoreDataModel.csdl|res://*/StoreDataModel.ssdl|
res://*/StoreDataModel.msl;provider=System.Data.SqlClient;
provider connection string=&quot;data source=(localdb)\v11.0;
initial catalog=StoreDB;integrated security=True;multipleactiveresultsets=True;
App=EntityFramework&quot;" providerName="System.Data.EntityClient" />

将实体类移至独立程序集

使用 EF 设计器创建的新模型会在同一个 DAL 项目中生成派生的 DbContext 和 POCO 类型实体类。为了获得更好的结构和组件可用性,我们可以将模型模板文件和所有实体类文件移到单独的项目或编译后的程序集中。要通过使用 运行自定义工具 上下文菜单命令在模型更新后自动刷新实体类内容,我们需要手动编辑数据上下文和数据模型模板文件中的一些代码行以使同步工作。以下是示例应用程序中的示例。

SM.Store.Service.DAL 项目的 StoreDataModel.Context.tt 文件中

SM.Store.Service.Entities 项目的 StoreDataModel.tt 文件中。

DAL 中结构更好的存储库和工作单元

在优化实现中,工作单元(UOW)对象应初始化数据上下文对象,并将其传递给存储库,以协调共享单个数据库上下文类的多个存储库的工作。由于 UOW 位于数据库上下文之上,因此可以将数据库连接字符串从调用者传递到 UOW 类,而不是直接注入到数据上下文类。

public class StoreDataModelUnitOfWork : IUnitOfWork
{
    private StoreDataEntities context;
    public StoreDataModelUnitOfWork(string connectionString)
    {
        this.context = new StoreDataEntities(connectionString);
    }
    //Other code...
}

为了完成此连接字符串注入,我们需要编辑数据上下文类构造函数的上下文模板 StoreDataModel.Context.tt 文件。每次保存文件或为模板运行自定义工具时,连接字符串注入代码都会保留在那里。

优化后的存储库结构应包括一个通用的基本存储库类,用于基本 CRUD 操作,以及其他继承通用存储库的特定实体类型的存储库。如果实体操作不超出通用存储库中的现有操作,则可能不需要该实体的派生存储库。可以从 BLL 直接调用通用存储库(参见下面 BLL 部分的示例)。

在通用存储库类中

public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
    public IUnitOfWork UnitOfWork { get; set; }
    public GenericRepository(IUnitOfWork unitOfWork)
    {
        this.UnitOfWork = unitOfWork;
    }
    //Other code...
}

Product 实体的单独类型化存储库类中

public class ProductRepository : 
GenericRepository<Entities.Product>, IProductRepository
{
    public ProductRepository(IUnitOfWork unitOfWork)
        : base(unitOfWork)
    { 
    }
    //Other code...
}

从业务逻辑层(BLL)调用多个存储库

尽管 BLL 的主要目的是强制执行业务规则,但我们也可以使用此层来管理使用多个存储库的操作,例如,以级联和事务方式将数据保存到多个实体中。这将使 DAL 保持清晰的单一实体相关结构。在 BLL 类中,多个存储库的实例将传递到构造函数中,以使用依赖注入模式(在下面的部分中介绍)。

public class CategoryBS : ICategoryBS
{
    private IGenericRepository<Entities.Category> _categoryRepository;
    private IProductRepository _productRepository;
    public CategoryBS(IGenericRepository<Entities.Category> 
    cateoryRepository, IProductRepository productRepository)
    {
        this._categoryRepository = cateoryRepository;
        this._productRepository = productRepository;
    }
    //Other code...
}

使用 Unity 配置依赖注入(DI)

使用 Microsoft Unity 库,我们可以轻松地实现依赖注入模式,以解耦服务的组件,同时保持这些组件的最大协同性。使用 Unity 进行 DI 的关键任务之一是在设计时或运行时配置容器。这两种配置方法之间没有性能差异,但设计时配置更灵活且易于维护。如果应用程序有针对不同数据源或服务层的类似组件,则使用设计时配置可以从 XML 配置文件中切换到所需的程序集,而无需重新编译和部署应用程序项目。

由于数据库连接字符串作为参数传递到 UOW 对象的构造函数中,因此运行时配置代码可以直接从 web.config 文件中的常规位置检索连接字符串值。但是,设计时配置无法通过相同的方式获取连接字符串。替代方法是在 Unity.config 文件中使用 Value 属性的占位符。在运行时,代码可以然后从 web.config 获取实际的连接字符串值,并替换 SM.Store.Service.DAL.StoreDataModelUnitOfWork 类构造函数中的占位符。

示例应用程序在下面的代码中演示了这两种配置选项的使用。

DIRegister.cs 文件中的运行时配置代码

public static void RegisterTypes(UnityContainer Container)
{
    //Register UnitOfWork with db ConnectionString injection
    string connString = ConfigurationManager.ConnectionStrings
                        ["StoreDataEntities"].ConnectionString;
    Container.RegisterType<DAL.IUnitOfWork, 
    DAL.StoreDataModelUnitOfWork>(new PerResolveLifetimeManager(), 
                                  new InjectionConstructor(connString));
    
    //Register BLL and DAL objects
    Container.RegisterType<BLL.IProductBS, BLL.ProductBS>();
    Container.RegisterType<DAL.IProductRepository, DAL.ProductRepository>();
    Container.RegisterType<BLL.ICategoryBS, BLL.CategoryBS>();
    Container.RegisterType<DAL.IGenericRepository<Entities.Category>, 
    DAL.GenericRepository<Entities.Category>>();
}

服务启动时,从 Global.asax.cs 调用该方法。

protected void Application_Start(object sender, EventArgs e)
{
    DIRegister.RegisterTypes(DIFactoryForRuntime.Container);
}

Unity.config 文件中的设计时配置代码

<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
  <assembly name="SM.Store.Service.DAL"/>
  <assembly name="SM.Store.Service.BLL"/>
  <alias alias="Category" 
  type="SM.Store.Service.Entities.Category, SM.Store.Service.Entities" />
  <container>
    <register type="SM.Store.Service.DAL.IUnitOfWork" 
    mapTo="SM.Store.Service.DAL.StoreDataModelUnitOfWork">
      <lifetime type="singleton" />
      <constructor>
        <!--Set placeholder for value attribute and replace it at runtime-->
        <param name="connectionString" value="{connectionString}" />
      </constructor>
    </register>   
    <register type="SM.Store.Service.BLL.IProductBS" 
    mapTo="SM.Store.Service.BLL.ProductBS"/>
    <register type="SM.Store.Service.DAL.IProductRepository" 
    mapTo="SM.Store.Service.DAL.ProductRepository"/>   
    <register type="SM.Store.Service.BLL.ICategoryBS" 
    mapTo="SM.Store.Service.BLL.CategoryBS"/>
    <register type="SM.Store.Service.DAL.IGenericRepository[Category]" 
    mapTo="SM.Store.Service.DAL.GenericRepository[Category]"/>   
  </container> 
</unity>

服务启动时,从 web.config 文件注册容器。

<configuration>
  <configSections>
    <section name="unity" 
    type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, 
    Microsoft.Practices.Unity.Configuration"/>
  </configSections>
  <unity configSource="Unity.config"/>
   . . .
</configuration>

SM.Store.Service.DAL.StoreDataModelUnitOfWork 类中替换占位符并使用实际连接字符串的代码

public StoreDataModelUnitOfWork(string connectionString)
{
    //Used for design time DI configuration
    if (connectionString == "{connectionString}")
        connectionString = ConfigurationManager.ConnectionStrings
                           ["StoreDataEntities"].ConnectionString;
    
    this.context = new StoreDataEntities(connectionString);
}

为了在示例应用程序中进行说明,一些服务方法使用运行时配置调用 Unity 容器来获取组件实例,而另一些服务方法使用设计时配置。在实际应用中,您可能只会在同一个应用程序中使用一种配置。

禁用延迟加载

默认情况下,数据上下文的延迟加载是启用的。对于封装在 Web 服务中的数据访问层,这可能不是最佳设置,因为在进行合同对象序列化之前的实体-合同类型转换过程中,会有多次与数据库服务器的交互。这可能会减慢类型转换过程。我们需要通过右键单击 **EF Designer** 的任意空白区域,选择 **Properties**,然后将 **Lazy Loading Enabled** 更改为 **False**,然后保存 EDMX 文件来禁用数据上下文的延迟加载。然后,我们可以在代码中使用以下方法。

  1. 如果需要,还可以包括子实体的延迟加载。SM.Store.Service.DAL.ProductRepository 类中的 GetProductList()GetProductByCategoryId() 方法以延迟加载为例。
  2. 在 LINQ to Entity 查询中联接链接实体,并使用自定义类型返回数据。SM.Store.Service.DAL.ProductRepository 类中的 GetFullProducts() 方法展示了此方法。
  3. 调用存储过程并使用 EF 复杂类型返回数据。示例应用程序包含更高级的 EF 存储过程映射,用于返回多个结果集。

这些替代方法将在后续章节中与相关主题一起讨论。

获取分页和排序的数据集

实现 EF 服务器端分页和排序需要构造适当的 LINQ 查询。排序是在进行分页时必须的。

排序中最受关注的问题是如何返回按子实体属性排序的记录。这可以通过搜索表达式树节点来查找子实体的属性,然后构建查询的 OrderBy 语句来完成。下面是 SM.Store.Service.Common.GenericSorterPager 类中用于构建此查询部分的方法。

public static IOrderedQueryable<T> AsSortedQueryable<T, TSortByType>
       (IQueryable<T> source, string sortBy, SortDirection sortDirection)
{
    //Initiate expression tree
    var param = Expression.Parameter(typeof(T), "item");
    Expression parent = param;

    //Separate child entity name and property
    string[] props = sortBy.Split(".".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

    //Reset expression tree if there is entity name prefix
    foreach (string prop in props)
    {
        parent = Expression.Property(parent, prop);
    }

    //Build the sort expression
    var sortExpression = Expression.Lambda<Func<T, TSortByType>>(parent, param);

    //Handle ascending and descending
    switch (sortDirection)
    {
        case SortDirection.Descending:
            return source.OrderByDescending<T, TSortByType>(sortExpression);
        default:
            return source.OrderBy<T, TSortByType>(sortExpression);
    }
}

分页中最受关注的问题是如何合并数据集和总计值的调用。EF 分页通常需要两次调用来获取请求的数据,一次是为了数据集,另一次是为了总计值,后者是填充客户端 UI 应用程序中的网格或表格显示结构所必需的。两次调用返回分页数据和计数数据可能会对大型数据库的性能产生影响。这个问题可以通过 SM.Store.Service.Common.GenericSorterPager 类中的 GetSortedPagedList() 方法中的代码来解决。

//Build one-call query from created regular query
var pagedGroup = from sel in sortedPagedQuery
                 select new PagedResultSet<T>()
                 {
                     PagedData = sel,
                     TotalCount = source.Count()
                 };
//Get the complete resultset from db.
List<PagedResultSet<T>> pagedResultSet = pagedGroup.AsParallel().ToList();
//Get data and total count from the resultset
IEnumerable<T> pagedList = new List<T>();

if (pagedResultSet.Count() > 0)
{
    totalCount = pagedResultSet.First().TotalCount;
    pagedList = pagedResultSet.Select(s => s.PagedData);
}
//Remove the wrapper reference
pagedResultSet = null;

return pagedList.ToList();

通过检查 <code>pagedGroup 变量值,EF 在后台生成的 SQL 查询如下所示。它实际上使用 Cross Join 来获取总计值,并将其附加到每一行。虽然这有点冗余,但不是问题,因为分页数据集只返回有限数量的行(在我们的示例中是 10 行)。

SELECT TOP (10) 
[Join3].[ProductID] AS [ProductID], 
[Join3].[C1] AS [C1], 
[Join3].[ProductName] AS [ProductName], 
[Join3].[CategoryID] AS [CategoryID], 
[Join3].[UnitPrice] AS [UnitPrice], 
[Join3].[StatusCode] AS [StatusCode], 
[Join3].[Description] AS [Description], 
[Join3].[A1] AS [C2]
FROM ( SELECT [Distinct1].[ProductID] AS [ProductID], _
[Distinct1].[ProductName] AS [ProductName], _
[Distinct1].[CategoryID] AS [CategoryID], [Distinct1].[UnitPrice] AS [UnitPrice], _
[Distinct1].[StatusCode] AS [StatusCode], [Distinct1].[Description] AS [Description], _
[Distinct1].[C1] AS [C1], [GroupBy1].[A1] AS [A1], row_number() _
OVER (ORDER BY [Distinct1].[ProductName] ASC) AS [row_number]
    FROM   (SELECT DISTINCT 
        [Extent1].[ProductID] AS [ProductID], 
        [Extent1].[ProductName] AS [ProductName], 
        [Extent1].[CategoryID] AS [CategoryID], 
        [Extent1].[UnitPrice] AS [UnitPrice], 
        [Extent1].[StatusCode] AS [StatusCode], 
        [Extent2].[Description] AS [Description], 
        1 AS [C1]
        FROM  [dbo].[Product] AS [Extent1]
        INNER JOIN [dbo].[ProductStatusType] AS [Extent2] _
        ON [Extent1].[StatusCode] = [Extent2].[StatusCode] ) AS [Distinct1]
    CROSS JOIN  (SELECT 
        COUNT(1) AS [A1]
        FROM ( SELECT DISTINCT 
            [Extent3].[ProductID] AS [ProductID], 
            [Extent3].[ProductName] AS [ProductName], 
            [Extent3].[CategoryID] AS [CategoryID], 
            [Extent3].[UnitPrice] AS [UnitPrice], 
            [Extent3].[StatusCode] AS [StatusCode], 
            [Extent4].[Description] AS [Description]
            FROM  [dbo].[Product] AS [Extent3]
            INNER JOIN [dbo].[ProductStatusType] AS [Extent4] _
            ON [Extent3].[StatusCode] = [Extent4].[StatusCode]
        )  AS [Distinct2] ) AS [GroupBy1]
)  AS [Join3]
WHERE [Join3].[row_number] > 0
ORDER BY [Join3].[ProductName] ASC

请注意,当使用基源 sortedPagedQuery 的延迟加载(在通用的 IQueriable<T> 类型中传递)时,pagedGroup 查询不会返回任何子实体的数据。它适用于延迟加载,但还需要额外的调用来获取子实体的数据。由于我们禁用了数据上下文的延迟加载,因此在使用延迟加载时,我们需要单独调用来获取总计值。GetSortedPagedList() 方法的代码处理了来自使用延迟加载和不加载子实体的查询的调用。

public static IList<T> GetSortedPagedList<T>(IQueryable<T> source, 
       PaginationRequest paging, out int totalCount, ChildLoad childLoad = ChildLoad.None)
{
    . . .
    //Call db for total count when child entity mode is eager loading
    if (childLoad == ChildLoad.Include)
    {
        totalCount = source.Count();
    }
    //Call to build sorted paged query
    IQueryable<T> sortedPagedQuery = GetSortedPagedQuerable<T>(source, paging);
    //Call db one time to get data rows and count together
    if (childLoad == ChildLoad.None)
    {
        //Build one-call query from created regular query
        [Code of one-call query shown before]
    }
    //Call db second time to get paged data rows when using eager loading
    else
    {
        return sortedPagedQuery.ToList();
    }
}

对象类型转换和 EF 查询场景

基于 SOAP 的 Web 服务通过合同对象的序列化和反序列化过程与服务使用者进行通信。但是,实体类型对象用于所有 BLL 和 DAL。实体-合同类型转换(反之亦然)在服务和客户端之间的每次请求和响应期间进行。这些转换也增加了服务数据操作的开销,特别是在以下情况下:

  • 当延迟加载启用时,检索包含子实体的数据集。在这种情况下,调用数据库获取子实体数据发生在映射过程中,这在数据访问 Web 服务中应避免。
  • 当返回大量行以及包含一个或多个(甚至多个级别)子实体时,对包含子实体的В 数据集使用延迟加载。转换器需要额外的工作来搜索和映射这些子实体属性。

对象类型之间属性映射的选项可以是自动、半自动和手动映射。我们可以使用 AutoMapper 库以通用方式执行自动和半自动映射,而手动转换则需要源对象和目标对象之间进行一对一的属性值分配。数据类型转换方法虽然在服务终结点方法中执行,但它们也与 DAL 中使用的 EF 查询相关。通常,使用自动映射可以简化开发和维护任务,但会降低性能。以下是示例应用程序中展示的几种映射场景。

//Obtain data as an entity object
Entities.Product ent = ProductBs.GetProductById(request.Id);

//Use simple auto mapping
IBaseConverter<Entities.Product, DataContracts.Product> 
convtResult = new AutoMapConverter<Entities.Product, DataContracts.Product>();

//Convert entity to contract object
resp.Product = convtResult.ConvertObject(ent);

此场景也用于所有将数据合同对象转换回实体对象的情况,用于 InsertUpdate 操作,这些操作都是基于单个实体的。

//Use custom configured auto mapping
ProductAutoMapConverter convtResult = new ProductAutoMapConverter();
List<DataContracts.ProductCM> dcList = convtResult.ConvertObjectCollection(productList);

ProductAutoMapConverter 类中的代码手动将子实体属性 ProductStatusType.Description 映射到合同对象的 StatusDescription 属性,以便在返回的数据列表中显示 StatusDescription 字段的数据值。其他所有属性均使用完全自动映射。

public class ProductAutoMapConverter : AutoMapConverter<Entities.Product, 
       DataContracts.ProductCM>
{
    public ProductAutoMapConverter()
    {
        //Mapping child entity property to contract property
        AutoMapper.Mapper.CreateMap<Entities.Product, DataContracts.ProductCM>()
         .ForMember(dest => dest.StatusDescription, _
         opt => opt.MapFrom(src => src.ProductStatusType.Description)); 
    }
}
public override DataContracts.ProductCM ConvertObject(Entities.Product ent)
{
    //Manual one-to-one mapping
    return new DataContracts.ProductCM()
    {
        ProductID = ent.ProductID,
        ProductName = ent.ProductName,
        CategoryID = ent.CategoryID != null ? ent.CategoryID.Value : 0,
        UnitPrice = ent.UnitPrice,
        StatusCode = ent.StatusCode,
        StatusDescription = ent.ProductStatusType != null ? 
                            ent.ProductStatusType.Description : string.Empty
    };
}
//Query to join parent and child entities and return custom object type
IQueryable<Entities.ProductCM> query = this.UnitOfWork.Context.Products
    .Join(this.UnitOfWork.Context.ProductStatusTypes,
     p => p.StatusCode, s => s.StatusCode,
    (p, s) => new Entities.ProductCM
    {
        ProductID = p.ProductID,
        ProductName = p.ProductName,
        CategoryID = p.CategoryID,
        UnitPrice = p.UnitPrice,
        StatusCode = p.StatusCode,
        StatusDescription = s.Description
    });

EF 查询负责在数据库中联接底层父表和子表,并使用父表和子表的所有字段作为自定义类型的属性返回数据。然后,我们可以轻松地使用简单的自动映射将 ProductCM 映射到其合同类型对应项。

//Use simple auto mapping
IBaseConverter<Entities.ProductCM, DataContracts.ProductCM> convtResult = 
               new AutoMapConverter<Entities.ProductCM, DataContracts.ProductCM>();

根据我的经验,这是 EF 查询和数据访问 WCF Web 服务中的数据类型转换的最佳场景,因为 DAL 只返回一个包含简单对象类型的集合,不包含任何其他子对象或链接对象。然后,我们可以避免在实体-合同类型转换过程中深入研究子实体来映射成员。在此场景中,我们也可以忽略是使用延迟加载还是延迟加载。

  1. 对不包含子实体的返回数据使用简单的自动映射。DAL 中的 GetProductById() 方法进行简单的查询调用,以获取不包含任何子实体属性的数据记录。
  2. 对返回包含任何子实体属性(在我们的情况下为延迟加载)的数据使用半自动或自定义配置的映射。此方法在 GetProductList() 服务方法中进行了演示。
  3. 使用纯手动映射。GetProductByCategoryId() 服务方法使用纯手动映射方法。它调用 ProductManualConverter 类,该类负责分配返回的每个属性。
  4. 使用优化的 EF 查询进行简单的自动映射。这需要一个自定义 POCO 对象,类似于 ViewModel 对象,该对象包含来自子实体的所有必需属性。在我们的情况下,它是 SM.Store.Service.Entities 项目中的 ProductCM 类。在 DAL 的 ProductReporsitory.GetFullProducts() 方法中,EF 查询定义如下。

从数据库存储过程返回多个结果集

示例应用程序展示了如何使用 EF 设计器函数导入映射来使用此功能。此部分仅包含 VS 2012/2013 的源代码,因为它仅受 .NET Framework 4.5 支持。在我的上一篇文章中详细介绍了实现细节,特别是手动编辑函数导入映射。

为了在实体端返回复杂类型,将响应数据合同对象和集合添加到 Web 服务项目中。

//Contract object to hold collections for multiple result sets
[DataContract(Namespace = Constants.NAMESPACE)]
public class CategoriesProductsResponse
{
    [DataMember(IsRequired = false, Order = 0)]
    public CategoriesCM Categories { get; set; }

    [DataMember(IsRequired = false, Order = 1)]
    public ProductsCM Products { get; set; }
}

//Collection for Category complex type
[CollectionDataContract(Namespace = Constants.NAMESPACE, 
Name = "CategoriesCM", ItemName = "CategoryCM")]
public class CategoriesCM : List<DataContracts.CategoryCM> { }

//Collection for Product complex type
[CollectionDataContract(Namespace = Constants.NAMESPACE, 
Name = "ProductsCM", ItemName = "ProductCM")]
public class ProductsCM : List<DataContracts.ProductCM> { }

然后,在类型转换之后,服务将返回多个结果集。由于复杂类型已经包含所有需要的字段,并且不需要从任何子数据结构进行字段映射,因此我们可以直接使用 AutoMapper 进行简单映射,以实现最佳性能和可维护性。

public DataContracts.CategoriesProductsResponse 
   GetCategoriesAndProducts(DataContracts.QueryByStringRequest request)
{
    DataContracts.CategoriesProductsResponse resp = 
                  new DataContracts.CategoriesProductsResponse();
    resp.Categories = new DataContracts.CategoriesCM();
    resp.Products = new DataContracts.ProductsCM();

    //Use design time configuration for Unity DI
    ICategoryBS categoryBS = DIFactoryForDesigntime.GetInstance<ICategoryBS>();

    //Get data by calling method in BLL.
    Entities.CategoriesProducts ent = categoryBS.GetCategoriesAndProducts();

    //Use simple auto mapping without custom configurations.
    IBaseConverter<Entities.Category_Result, DataContracts.CategoryCM> convtResult1 = 
         new AutoMapConverter<Entities.Category_Result, DataContracts.CategoryCM>();
    List<DataContracts.CategoryCM> dcList1 = 
             convtResult1.ConvertObjectCollection(ent.Categories);
    IBaseConverter<Entities.Product_Result, DataContracts.ProductCM> convtResult2 = 
         new AutoMapConverter<Entities.Product_Result, DataContracts.ProductCM>();
    List<DataContracts.ProductCM> dcList2 = convtResult2.ConvertObjectCollection(ent.Products);
    
    //Add multiple collections to resp object and return it.
    resp.Categories.AddRange(dcList1);
    resp.Products.AddRange(dcList2);
    return resp;
}

从客户端运行和测试服务

在本地开发环境中,示例 Web 服务从 Visual Studio 2010 的开发服务器和 Visual Studio 2012/2013 的 IIS Express 运行。要从任何外部客户端应用程序在本地测试服务,您首先需要通过指向服务终结点 URL 的浏览器运行 Web 服务。最简单的方法是从 Visual Studio Solution Explorer 中对终结点文件(*.svc)执行 **在浏览器中查看** 上下文菜单命令。然后,您可以使用客户端应用程序或工具(例如 WcfTestClient.exe(位于 C:\Program Files (x86)\Microsoft Visual Studio [版本号]\Common7\IDE)或 SoapUI)来测试服务。如果打开 WcfTestClient,将“https://:23066/Services/ProductService.svc”终结点 URL 添加到其中,然后选择并调用 GetProductByCategoryId() 服务方法,结果屏幕应如下所示。

示例应用程序还在客户端包含了一个单元测试项目。它将服务引用项 CategorySvcRefProductSvcRef 设置为指向服务终结点的客户端代理。该项目作为服务测试客户端具有许多优点:

  • 自动启动服务。当将测试项目设置为启动项目并在同一个 Visual Studio 解决方案中运行时,您无需在运行测试之前从浏览器手动启动服务,因为服务将在那里自动运行。
  • 进行手动或自动单元测试。
  • 用于轻松调试。通过测试类中执行的代码行,我们可以单步调试服务解决方案中任何项目的代码。
  • 检查返回数据的详细信息。当在返回数据后的某一行设置断点时,我们可以在客户端工具提示、自动窗口或本地窗口中查看所有数据详细信息。

使用基于任务的异步操作调用服务

基于任务的异步模式(TAP)是新开发推荐的异步设计模式。使用 .NET Framework 4.5,其结构和语法得到了简化,可以实现 TAP。要异步调用服务方法,我们无需在服务器端执行任何操作。我们只需要在添加新的、或更新/配置现有服务引用时,在 **服务引用设置** 对话框中启用 **允许生成异步操作** 和 **生成基于任务的操作**。然后,客户端代理会自动添加具有后缀 Async 并返回可等待的 Task 类型的相应方法,用于 TAP 操作。

示例应用程序中的 TestClientConsole 项目包含用于调用 GetFullProductsAsync() 服务方法的代码,该方法由 GetFullProductsTaskAsync() 方法包装,并带有 async 关键字,该方法开启另一个线程以异步处理数据检索。Console.Write() 代码行物理上位于主线程中调用 GetFullProductsTaskAsync() 之后,但它在数据检索完成并显示之前继续运行并显示文本。

static void Main(string[] args)
{
    //Call async method
    GetFullProductsTaskAsync();

    //Previous call in another thread gets waiting and this line executed before it.
    Console.Write("This code line after async call in main thread 
                   \r\nbut was executed before returning async data\r\n");
    Console.ReadKey();
}
private async static void GetFullProductsTaskAsync()
{
    //Initiate client proxy object and construct input request
     . . .
    //Processing data retrieval in another thread
    Task<ProductListResponse> taskResp = client.GetFullProductsAsync(inputItem);
    var resp = await taskResp;

    //Display data
    Console.WriteLine("");
    Console.WriteLine("Products: (total {0}, listed {1})", 
                       resp.TotalCount.ToString(), resp.Products.Count.ToString());
    foreach (var item in resp.Products)
    {
        Console.WriteLine("");
        Console.WriteLine("Name: {0}", item.ProductName);
        Console.WriteLine("Price: ${0}", item.UnitPrice.ToString());
    }
}

摘要

WCF 和 EF 数据访问 Web 服务的许多方面都可以改进或增强,以使应用程序更健壮、更有价值。我希望本文对有兴趣或正在从事数据层服务应用程序的开发人员有所帮助。

历史

  • 2013 年 11 月 21 日:初始版本
© . All rights reserved.