RESTful WCF / EF POCO / 工作单元 / 存储库 / MEF:1/2






4.97/5 (113投票s)
探讨如何使用良好的实践和MSFT技术栈来设计服务层。
目录
引言
这篇文章比较特别,有些牵强,但许多人要求我创建一个LOB(业务线)应用程序,以展示我通常会在前端和数据库之间如何组织我的层(这主要是由我的Cinch MVVM框架的用户提出的)。
虽然这篇文章使用了一个简单的控制台应用程序作为前端,但我的意图是将其作为一篇两部分的文章,我将在有更多时间时进行回顾和扩展,这可能需要一段时间,因为我正在处理一项比本文更庞大的工作。
即便如此,这篇文章展示了我经常使用的一些东西,我认为它可能是一篇不错的文章,所以我想我现在就写下我能写的部分,剩下的部分将在准备好后发布。
那么这个演示应用程序到底做了什么?简单来说,它是一个简单的控制台应用程序,用于从单个数据库表中检索和添加数据。它使用了LINQ to Entities和领域驱动设计(DDD),因此利用了存储库模式,并且还包含了一些额外的功能,例如如何使用一些典型的企业级功能,如
- 控制反转(我正在使用新的MEF版本)
- log4Net
- 工作单元与存储库的协同工作
- RESTful WCF,使用新的WCF WebApi
注意:我特意使此演示代码非常精简,以便人们可以轻松理解概念,学习东西时,没有什么比在大量代码中摸索更糟糕的了。所以在阅读文章的其余部分时,请牢记这一点。
先决条件
有相当多的先决条件,但其中大部分都包含在随附的演示代码中。下表列出了所有先决条件,并告诉您它们是否包含在随附的演示代码中,或者您是否真的必须拥有它们才能运行代码。
项目 | 已包含 |
SQL Server | 否 您必须已经拥有此项。 |
Entity Framework 4.1 POCO | 是 请参阅Lib\Entity Framework 4.1\EntityFramework41.exe。 |
log4Net | 是 请参阅Lib\Log4Net\1.2.10.0\log4net.dll。 |
Lib\MEF 2 | 是 请参阅Lib\MEF 2\System.ComponentModel.Composition.CodePlex.dll 请参阅Lib\MEF 2\System.ComponentModel.Composition.Registration.CodePlex.dll 请参阅Lib\MEF 2\System.ComponentModel.Composition.Web.Mvc.CodePlex.dll 请参阅Lib\MEF 2\System.Reflection.Context.CodePlex.dll |
WCF Web API Preview 4 | 是 请参阅 Lib\WCF Web API Preview 4 但说实话,您最好从Nuget包中获取这些东西。 |
现在,有些人可能会注意到其中一些可能存在更新的版本,虽然这是事实,但本文的代码是从我正在进行的更大项目的一部分提取出来的,我知道这些版本可以协同工作,所以我选择了这些版本。
总体设计 / 入门
基本设计非常简单,工作方式如下:
我们有一个简单的两表数据库(您可以使用随附的演示代码脚本进行设置),我们希望用户能够对其进行添加和查询。我们希望能够使用WCF服务来实现这一点,并使用现代技术,如IOC/日志记录,提供清晰的关注点分离,同时保持能够通过替换实现或使用模拟来修改/测试系统的任何部分的能力。
此图说明了随附演示项目的核心层。正如我之前所说,随附的演示项目非常简单,这是故意的,但即使只有两个表和没有实际的业务逻辑,我们也能够满足上述所有要求。
入门
要开始,您需要执行以下操作:
- 创建一个名为“TESTEntites”的新SQL Server数据库,您可以使用“getting started”解决方案文件夹中的两个脚本来完成。
- 更改Restful.WCFService中的Web.Config以指向您新创建的数据库和SQL Server安装。
RESTful API
Glenn Block和他的团队创建的WCF WebApi是一件很酷的事情,它将我们熟悉和喜爱的WCF领域的东西带到了web上,并允许它们通过JSON或XML序列化轻松地暴露出来。
早在.NET 3.5 SP1中,我们就可以使用有限数量的属性来实现这一点,例如在标准的WCF服务上使用WebGet
/WebInvoke
,然后可以将其托管在一个特殊的Web服务主机中,即WebServiceHost
。这在当时很酷,我写了以下链接的文章,如果您想看看的话:
- http://sachabarber.net/?p=460
- http://sachabarber.net/?p=475
- https://codeproject.org.cn/KB/smart/GeoPlaces.aspx
但那时已是过去,现在不同了。现在有Glenn团队开发的新的WCF WebApi。
那么这个新的产品有什么酷之处呢?嗯,以下是我认为的优点:
- 它本质上只是一个WCF服务
- 它实际上不需要特殊的宿主
- 它确实是RESTful的
- 可以使用简单的客户端JavaScript代码(如jQuery)调用
- 支持oData(允许使用URL进行简单查询)
我认为这些都是考虑它的好理由。
那么这样的服务是什么样的呢?嗯,对于演示代码服务,完整的代码如下(注意:它使用了MEF / 工作单元模式 / 存储库模式 / oData / EF 4.1,我们稍后将讨论所有这些内容)。
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Net;
using System.Net.Http;
using Microsoft.ApplicationServer.Http.Dispatcher;
using Restful.Models;
using Restful.Models.Dtos;
using System.ComponentModel.Composition;
using Restful.Entities;
using Restful.WCFService.Services.Contracts;
using Models.Utils;
namespace Restful.WCFService.WebApi
{
[ServiceContract]
[Export]
public class EFResource : IDisposable
{
IUnitOfWork unitOfWork;
IRepository<Author> authorsRep;
IRepository<Book> booksRep;
ILoggerService loggerService;
[ImportingConstructor]
public EFResource(
IUnitOfWork unitOfWork,
IRepository<Author> authorsRep,
IRepository<Book> booksRep,
ILoggerService loggerService)
{
this.unitOfWork = unitOfWork;
this.authorsRep = authorsRep;
this.booksRep = booksRep;
this.loggerService = loggerService;
}
[WebGet(UriTemplate = "Authors")]
public IQueryable<DtoAuthor> GetAuthors()
{
loggerService.Info("Calling IQueryable<DtoAuthor> GetAuthors()");
authorsRep.EnrolInUnitOfWork(unitOfWork);
List<Author> authors =
authorsRep.FindAll("Books").ToList();
IQueryable<DtoAuthor> dtosAuthors = authors
.Select(a => DtoTranslator.TranslateToDtoAuthor(a))
.ToList().AsQueryable<DtoAuthor>();
return dtosAuthors;
}
[WebGet(UriTemplate = "Books")]
public IQueryable<DtoBook> GetBooks()
{
loggerService.Info("Calling IQueryable<DtoBook> GetBooks()");
booksRep.EnrolInUnitOfWork(unitOfWork);
List<Book> books =
booksRep.FindAll().ToList();
IQueryable<DtoBook> dtosBooks = books
.Select(b => DtoTranslator.TranslateToDtoBook(b))
.ToList().AsQueryable<DtoBook>();
return dtosBooks;
}
[WebInvoke(UriTemplate = "AddAuthor", Method = "POST")]
public Restful.Models.Dtos.DtoAuthor AddAuthor(DtoAuthor dtoAuthor)
{
loggerService.Info("Restful.Models.Author AddAuthor(DtoAuthor dtoAuthor)");
if (dtoAuthor == null)
{
loggerService.Error("author parameter is null");
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
authorsRep.EnrolInUnitOfWork(unitOfWork);
Author author = EfTranslator.TranslateToEfAuthor(dtoAuthor);
authorsRep.Add(author);
unitOfWork.Commit();
dtoAuthor.Id = author.Id;
return dtoAuthor;
}
[WebInvoke(UriTemplate = "AddBook", Method = "POST")]
public Restful.Models.Dtos.DtoBook AddBook(DtoBook dtoBook)
{
loggerService.Info("Restful.Models.Book AddBook(DtoBook dtoBook)");
if (dtoBook == null)
{
loggerService.Error("book parameter is null");
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
booksRep.EnrolInUnitOfWork(unitOfWork);
Book book = EfTranslator.TranslateToEfBook(dtoBook);
booksRep.Add(book);
unitOfWork.Commit();
dtoBook.Id = book.Id;
return dtoBook;
}
public void Dispose()
{
unitOfWork.Dispose();
}
}
}
可以看到,如果我们去掉方法中的实际代码,就只剩下几个可以通过调用URL来调用的方法。例如,添加一个新的EF 4.1 Book,我只需这样做:
https://:8300/ef/AddBook
其中POST请求将包含一个新的Book
对象。
那么如何获取所有Book
对象的GET请求呢?就是这样的GET请求:
https://:8300/ef/Books
这是浏览器中的一个截图,我刚刚导航到那个URL。您可以看到它显示了我数据库安装中的所有书籍(您需要自己设置数据库)。
您可能会注意到这是XML格式。通过更改WebGet
和WebInvoke
属性的RequestFormat
、ResponseFormat
和BodyFormat
,可以轻松更改此格式。这些属性用于服务。
那么关于oData支持呢?事实上,如果您不知道oData是什么,以下是www.oData.org的解释:
如今有大量的数据可用,并且数据正在以前所未有的速度被收集和存储。然而,这些数据中的大部分(如果不是全部)都锁定在特定的应用程序或格式中,难以访问或集成到新的用途中。
开放数据协议(OData)是一种用于查询和更新数据的Web协议,它提供了一种解锁数据并摆脱当今应用程序中存在的孤岛的方法。OData通过应用和构建于HTTP、Atom发布协议(AtomPub)和JSON等Web技术之上,来提供对各种应用程序、服务和存储中的信息的访问。该协议源于过去几年在各种产品中实现AtomPub客户端和服务器的经验。OData已被用于公开和访问来自各种来源的信息,包括但不限于关系数据库、文件系统、内容管理系统和传统网站。
OData与Web的工作方式一致——它对用于资源标识的URI做出了深刻的承诺,并致力于提供一个基于HTTP的统一接口来与这些资源进行交互(就像Web一样)。这种对核心Web原则的承诺使得OData能够在广泛的客户端、服务器、服务和工具之间实现新水平的数据集成和互操作性。
好了,这也内置了,我们只需要提供一些额外的部分。例如,这是我如何获取最上面的那本书:
https://:8300/ef/Books?$top=1
这会产生以下结果:
至于托管这类服务,几乎没什么复杂的。这里有一段Web.Config代码:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.serviceModel>
<serviceHostingEnvironment
aspNetCompatibilityEnabled="true" />
</system.serviceModel>
</configuration>
以及这段Global.asax.cs代码(如果您使用C#),就这些了。
using Microsoft.ApplicationServer.Http.Activation;
using System;
using System.ServiceModel.Activation;
using System.Web.Routing;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Registration;
using System.ComponentModel.Composition;
using Restful.Entities;
using Restful.WCFService.Services.Implementation;
using Restful.WCFService.Services.Contracts;
using log4net.Config;
using Restful.WCFService.Mef;
using Restful.WCFService.WebApi;
namespace Restful.WCFService
{
public class Global : System.Web.HttpApplication
{
private void Application_Start(object sender, EventArgs e)
{
// setting up Web Api WCF Service route
RouteTable.Routes.MapServiceRoute<EFResource>("ef");
}
}
}
另一个值得关注的点是,我们从不直接序列化和发送原始的Entity Framework POCO类,而是使用“数据传输对象”(DTOs),我们将它们与Entity Framework POCO类相互转换。经验法则是:
- Entity Framework POCO类:用于服务器端持久化
- DTO类:从/发送到WCF WebApi服务
如何保护REST服务
这篇文章的一位读者指出我没有提到安全性,这是一个有效的问题,我根本没提到。主要原因是我为我的OSS项目使用的是ASP MVC,而不是WCF WebApi,在那里我钩入了标准的ASP授权方法。
即便如此,同一位读者(RAbbit12,谢谢)也提供了一个很好的链接,讨论了如何使用属性(类似于ASP MVC实际做的那样)来为WCF WebApi服务应用授权。这是该文章的链接:http://haacked.com/archive/2011/10/19/implementing-an-authorization-attribute-for-wcf-web-api.aspx。
MEF
在工作场所设计系统时,我们通常选择抽象掉任何系统边界,以便在系统出现故障或我们希望使用某个系统的测试版本时,可以轻松地替换或模拟这些边界。我们通常期望能够从最高级别做到这一点,并且所有依赖项都从IOC容器中解析。对我们来说,这意味着我们期望整个WCF服务都属于IOC容器,它会接受对其他服务/存储库或帮助器的依赖。
我们发现将这些服务/存储库/帮助器抽象到接口后面,大大提高了我们替换系统任何部分的能力,并能够创建它们的模拟版本。MEF是我目前选择的IOC容器,因为我喜欢其元数据,所以本文将重点介绍这一点。
以下各节将讨论我们有一天将在.NET中获得的新MEF API。
导入
在使用MEF时,需要做的第一件事是定义我们要使用的Import
/Export
。MEF V2与V1相比有所变化,因为我们不再需要为类提供ImportAttribute
和ExportAttribute
标记,所有这些都应通过新的、改进的注册过程来处理,但我仍然喜欢使用它们,因为它们清楚地向我展示了东西的来源以及在它们不起作用时我可以去哪里进行故障排除。
这是演示WCF WebApi及其所有预期的Import
s,您可能还会注意到整个WCF WebApi服务也被Export
ed了。这允许在获得此服务的实例时满足整个依赖图。相信我,这就是你想要的,手动获取的服务定位对象在一个测试中丢失一个的话会很麻烦。
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Net;
using System.Net.Http;
using Microsoft.ApplicationServer.Http.Dispatcher;
using Restful.Models;
using Restful.Models.Dtos;
using System.ComponentModel.Composition;
using Restful.Entities;
using Restful.WCFService.Services.Contracts;
using Models.Utils;
namespace Restful.WCFService.WebApi
{
[ServiceContract]
[Export]
public class EFResource : IDisposable
{
IUnitOfWork unitOfWork;
IRepository<Author> authorsRep;
IRepository<Book> booksRep;
ILoggerService loggerService;
[ImportingConstructor]
public EFResource(
IUnitOfWork unitOfWork,
IRepository<Author> authorsRep,
IRepository<Book> booksRep,
ILoggerService loggerService)
{
this.unitOfWork = unitOfWork;
this.authorsRep = authorsRep;
this.booksRep = booksRep;
this.loggerService = loggerService;
}
[WebGet(UriTemplate = "Authors")]
public IQueryable<DtoAuthor> GetAuthors()
{
loggerService.Info("Calling IQueryable<DtoAuthor> GetAuthors()");
authorsRep.EnrolInUnitOfWork(unitOfWork);
List<Author> authors =
authorsRep.FindAll("Books").ToList();
IQueryable<DtoAuthor> dtosAuthors = authors
.Select(a => DtoTranslator.TranslateToDtoAuthor(a))
.ToList().AsQueryable<DtoAuthor>();
return dtosAuthors;
}
[WebGet(UriTemplate = "Books")]
public IQueryable<DtoBook> GetBooks()
{
loggerService.Info("Calling IQueryable<DtoBook> GetBooks()");
booksRep.EnrolInUnitOfWork(unitOfWork);
List<Book> books =
booksRep.FindAll().ToList();
IQueryable<DtoBook> dtosBooks = books
.Select(b => DtoTranslator.TranslateToDtoBook(b))
.ToList().AsQueryable<DtoBook>();
return dtosBooks;
}
[WebInvoke(UriTemplate = "AddAuthor", Method = "POST")]
public Restful.Models.Dtos.DtoAuthor AddAuthor(DtoAuthor dtoAuthor)
{
loggerService.Info("Restful.Models.Author AddAuthor(DtoAuthor dtoAuthor)");
if (dtoAuthor == null)
{
loggerService.Error("author parameter is null");
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
authorsRep.EnrolInUnitOfWork(unitOfWork);
Author author = EfTranslator.TranslateToEfAuthor(dtoAuthor);
authorsRep.Add(author);
unitOfWork.Commit();
dtoAuthor.Id = author.Id;
return dtoAuthor;
}
[WebInvoke(UriTemplate = "AddBook", Method = "POST")]
public Restful.Models.Dtos.DtoBook AddBook(DtoBook dtoBook)
{
loggerService.Info("Restful.Models.Book AddBook(DtoBook dtoBook)");
if (dtoBook == null)
{
loggerService.Error("book parameter is null");
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
booksRep.EnrolInUnitOfWork(unitOfWork);
Book book = EfTranslator.TranslateToEfBook(dtoBook);
booksRep.Add(book);
unitOfWork.Commit();
dtoBook.Id = book.Id;
return dtoBook;
}
public void Dispose()
{
unitOfWork.Dispose();
}
}
}
配置
就满足对象图和托管该服务而言,这非常简单,多亏了新的WCF WebApi,我们只需要在Global.asax.cs中添加这一行(如果您使用C#的话)。
RouteTable.Routes.MapServiceRoute<EFResource>("ef");
这只是将WCF WebApi服务公开为ASP MVC路由。
所以,这基本上是我们公开/托管WCF WebApi服务并使其可调用的所有内容。但是如何从顶向下满足依赖关系树呢?嗯,这可以通过在Global.asax.cs中使用以下WCF WebApi代码来完成:
var config = new MefConfiguration(container);
config.EnableTestClient = true;
RouteTable.Routes.SetDefaultHttpConfiguration(config);
注意那里使用了MefConfiguration
类。嗯,这就是我们用来确保MEF能够满足不仅是WCF WebApi服务,而且所有必需的MEF组件的Import/Export
需求。
那么让我们来看看这个类,好吗?这是它的全部内容:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.ApplicationServer.Http;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Primitives;
namespace Restful.WCFService.Mef
{
public class MefConfiguration : WebApiConfiguration
{
public MefConfiguration(CompositionContainer container)
{
CreateInstance = (t, i, m) =>
{
var contract = AttributedModelServices.GetContractName(t);
var identity = AttributedModelServices.GetTypeIdentity(t);
// force non-shared so that every service doesn't need to
// have a [PartCreationPolicy] attribute.
var definition = new ContractBasedImportDefinition(contract,
identity, null, ImportCardinality.ExactlyOne, false, false,
CreationPolicy.NonShared);
return container.GetExports(definition).First().Value;
};
ReleaseInstance = (i, o) =>
{
(o as IDisposable).Dispose();
Lazy<EFResource> service = new Lazy<EFResource>(() =>
{
return (EFResource)o;
});
container.ReleaseExport<EFResource>(service);
};
}
}
}
可以看出,这段代码负责创建MEF的ContractBasedImportDefinition
和实际的Export
ed值。
注册
MEF以前通过开发者选择使用的任何属性来自动连接所有的Import/Exports。这些属性仍然可用,但注册过程发生了很大变化,它现在更类似于其他流行的IOC提供商,如Castle/AutoFac等。
以下是我们将如何提供MEF容器在从容器解析类型时实际使用的组件。
让我们再次查看演示代码WCF WebApi服务,其中我们有这个:
[ServiceContract]
[Export]
public class EFResource : IDisposable
{
IUnitOfWork unitOfWork;
IRepository<Author> authorsRep;
IRepository<Book> booksRep;
ILoggerService loggerService;
[ImportingConstructor]
public EFResource(
IUnitOfWork unitOfWork,
IRepository<Author> authorsRep,
IRepository<Book> booksRep,
ILoggerService loggerService)
{
this.unitOfWork = unitOfWork;
this.authorsRep = authorsRep;
this.booksRep = booksRep;
this.loggerService = loggerService;
}
....
....
....
....
}
我们如何确保MEF 2能够满足所有这些要求?这很简单,我们只需要在MEF容器中注册我们想要的内容,如下所示:
private void Application_Start(object sender, EventArgs e)
{
// use MEF for providing instances
RegistrationBuilder context = new RegistrationBuilder();
XmlConfigurator.Configure();
context.ForType(typeof(Log4NetLoggerService)).
Export(builder => builder.AsContractType(typeof(ILoggerService)))
.SetCreationPolicy(CreationPolicy.Shared);
context.ForType(typeof(Repository<>))
.Export(builder => builder.AsContractType(typeof(IRepository<>)))
.SetCreationPolicy(CreationPolicy.NonShared);
context.ForType(typeof(TESTEntities))
.Export(builder => builder.AsContractType(typeof(IUnitOfWork)))
.SetCreationPolicy(CreationPolicy.NonShared);
AggregateCatalog catalog = new AggregateCatalog(
new AssemblyCatalog(typeof(Global).Assembly, context),
new AssemblyCatalog(typeof(Repository<>).Assembly, context));
var container = new CompositionContainer(catalog,
CompositionOptions.DisableSilentRejection | CompositionOptions.IsThreadSafe);
var config = new MefConfiguration(container);
config.EnableTestClient = true;
RouteTable.Routes.SetDefaultHttpConfiguration(config);
}
注意新的注册语法的使用,另外请注意,我们能够处理IRepository
类型的开放泛型支持,其中通用类型在我们在使用MEF 2容器注册的IRepository
类型时指定。仅凭这个功能,就值得迁移到MEF 2,在我看来。
正如您在演示代码WCF WebApi服务中看到的,它有两种不同的IRepository
类型,每种类型使用不同的泛型类型。非常方便。
现在我们有了所有拼图块,这样当我们运行WCF WebApi服务时,我们会看到它已完全加载了它的需求。
EF 4.1 POCO
LINQ to Entities已经存在很长时间了,并且经历了许多不同的版本(至少我认为是这样)。事实是,直到新的LINQ to EF 4.1 POCO版本出现之前,它们都不能真正吸引我。这允许我们:
- 使用一个非常简单的数据上下文对象,它只包含数据对象的集合。
- 不使用任何形式的代码生成(其中任何东西都会被塞入生成的对象中)。
- 使用简单的手工编写的代码类,这些类不包含数据库的关注点。
使用这种POCO代码优先的方法,我们可以创建一个简单的数据上下文类,例如这个(我们将在几分钟内详细介绍)。
让我们从检查实际的LINQ to Entities 4.1 POCO上下文对象开始。它对我们来说,生命始于一个扩展了DbContext
的类。
这是演示中的类:
?using System;;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
namespace Restful.Entities
{
public abstract class EfDataContextBase : DbContext, IUnitOfWork
{
public EfDataContextBase(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
public IQueryable<T> Get<T>() where T : class
{
return Set<T>();
}
public bool Remove<T>(T item) where T : class
{
try
{
Set<T>().Remove(item);
}
catch (Exception)
{
return false;
}
return true;
}
public void Commit()
{
base.SaveChanges();
}
public void Attach<T>(T obj) where T : class
{
Set<T>().Attach(obj);
}
public void Add<T>(T obj) where T : class
{
Set<T>().Add(obj);
}
}
}
这提供了一个基本的Entity Framework基类。但我们需要进一步扩展它,为我们的EF 4.1 POCO对象创建一个特定的实现。所以我们得到这个:
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.ComponentModel.Composition;
using Restful.Models;
namespace Restful.Entities
{
public class ApplicationSettings
{
[Export("EFConnectionString")]
public string ConnectionString
{
get { return "name=TESTEntities"; }
}
}
public partial class TESTEntities : EfDataContextBase, IUnitOfWork
{
[ImportingConstructor()]
public TESTEntities(
[Import("EFConnectionString")]
string connectionString)
: base(connectionString)
{
this.Configuration.ProxyCreationEnabled = false;
this.Configuration.LazyLoadingEnabled = true;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<Author> Authors { get; set; }
public DbSet<Book> Books { get; set; }
}
}
如果我们接着检查实际的EDMX文件,我们可以看到代码生成已关闭:
这意味着我们必须开发自己的类,这些类可以与此LINQ to Entities EDMX模型文件一起使用。可以看出,该模型包含两个类:Author
和Book
,它们都显示在下面。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
namespace Restful.Models
{
public partial class Author
{
public Author()
{
this.Books = new List<Book>();
}
public int Id { get; set; }
public string Name { get; set; }
public List<Book> Books { get; set; }
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture,
"Author Name: {0}, Author Id: {1}", Name, Id);
}
}
}
这是Book
类:
using System;
using System.Collections.Generic;
using System.Globalization;
namespace Restful.Models
{
public partial class Book
{
public int Id { get; set; }
public string Title { get; set; }
public int AuthorId { get; set; }
public Author Author { get; set; }
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture,
"Title: {0}, Book Id: {1}", Title, Id);
}
}
}
工作单元
此模式跟踪在影响数据库的业务事务期间发生的所有事情。在事务结束时,它决定如何更新数据库以符合更改。
Martin Fowler有一篇关于此的精彩文章:https://martinfowler.com.cn/eaaCatalog/unitOfWork.html。
既然我们正在使用LINQ to Entities 4.1 POCO,我们已经具备了一些创建良好的工作单元模式实现所需的组件。
和以前一样,我们从检查实际的LINQ to Entities 4.1 POCO上下文对象开始。它对我们来说,生命始于一个扩展了DbContext
的类。
这是演示中的类:
?using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
namspace Restful.Entities
{
public abstract class EfDataContextBase : DbContext, IUnitOfWork
{
public EfDataContextBase(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
public IQueryable<T> Get<T>() where T : class
{
return Set<T>();
}
public bool Remove<T>(T item) where T : class
{
try
{
Set<T>().Remove(item);
}
catch (Exception)
{
return false;
}
return true;
}
public void Commit()
{
base.SaveChanges();
}
public void Attach<T>(T obj) where T : class
{
Set<T>().Attach(obj);
}
public void Add<T>(T obj) where T : class
{
Set<T>().Add(obj);
}
}
}
您现在知道,这提供了一个基本的Entity Framework工作单元基类。但我们需要进一步扩展它,为我们的EF 4.1 POCO对象创建一个特定的实现。所以我们得到这个:
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.ComponentModel.Composition;
using Restful.Models;
namespace Restful.Entities
{
public class ApplicationSettings
{
[Export("EFConnectionString")]
public string ConnectionString
{
get { return "name=TESTEntities"; }
}
}
public partial class TESTEntities : EfDataContextBase, IUnitOfWork
{
[ImportingConstructor()]
public TESTEntities(
[Import("EFConnectionString")]
string connectionString)
: base(connectionString)
{
this.Configuration.ProxyCreationEnabled = false;
this.Configuration.LazyLoadingEnabled = true;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<Author> Authors { get; set; }
public DbSet<Book> Books { get; set; }
}
}
基本思想是,我们只公开DbSet
属性以及Add/Remove和Save更改的方法。与这些方法的交互将通过将存储库注册到工作单元来完成,我们将在稍后看到这一点。
存储库
存储库模式已经存在很长时间了,并且源于领域驱动设计。MSDN关于存储库模式目标的说法是:
使用存储库模式来实现以下一个或多个目标:
- 您希望最大化可用于自动化测试的代码量,并隔离数据层以支持单元测试。
- 您从许多位置访问数据源,并希望应用集中管理的一致的访问规则和逻辑。
- 您希望实现和集中化数据源的缓存策略。
- 您希望通过将业务逻辑与数据或服务访问逻辑分离来提高代码的可维护性和可读性。
- 您希望使用强类型的业务实体,以便在编译时而不是运行时识别问题。
- 您希望将行为与相关数据关联。例如,您希望计算字段或强制执行实体内数据元素之间的复杂关系或业务规则。
- 您希望应用领域模型来简化复杂的业务逻辑。
听起来很酷,不是吗?但这如何转化为代码呢?嗯,这是我的看法(同样,我将其抽象到接口后面,以允许替代实现或模拟):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
using System.Linq.Expressions;
using System.ComponentModel.Composition;
namespace Restful.Entities
{
public class Repository<T> : IRepository<T> where T : class
{
protected IUnitOfWork context;
public void EnrolInUnitOfWork(IUnitOfWork unitOfWork)
{
this.context = unitOfWork;
}
public int Count
{
get { return context.Get<T>().Count(); }
}
public void Add(T item)
{
context.Add(item);
}
public bool Contains(T item)
{
return context.Get<T>().FirstOrDefault(t => t == item) != null;
}
public void Remove(T item)
{
context.Remove(item);
}
public IQueryable<T> FindAll()
{
return context.Get<T>();
}
public IQueryable<T> FindAll(Func<DbSet<T>, IQueryable<T>> lazySetupAction)
{
DbSet<T> set = ((DbSet<T>)context.Get<T>());
return lazySetupAction(set);
}
public IQueryable<T> FindAll(string lazyIncludeStrings)
{
DbSet<T> set = ((DbSet<T>)context.Get<T>());
return set.Include(lazyIncludeStrings).AsQueryable<T>();
}
public IQueryable<T> FindBy(Func<T, bool> predicate)
{
return context.Get<T>().Where(predicate).AsQueryable<T>();
}
public IQueryable<T> FindByExp(Expression<Func<T, bool>> predicate)
{
return context.Get<T>().Where(predicate).AsQueryable<T>();
}
public IQueryable<T> FindBy(Func<T, bool> predicate, string lazyIncludeString)
{
DbSet<T> set = (DbSet<T>)context.Get<T>();
return set.Include(lazyIncludeString).Where(predicate).AsQueryable<T>();
}
public IQueryable<T> FindByExp(Expression<Func<T,
bool>> predicate, string lazyIncludeString)
{
return context.Get<T>().Where(predicate).AsQueryable<T>();
}
}
}
乍一看,这可能令人困惑,但存储库所做的就是抽象与数据库原始数据打交道。您也可以从上面看到,我的实现依赖于一个IUnitOfWork
对象。在我的例子中,这就是我们用来与数据库通信的实际LINQ to Entities 4.1 DbContext
类。
因此,通过允许您的存储库与IUnitOfWork
(LINQ to Entities 4.1 DbContext
)代码进行交互,我们可以让存储库注册到一个简单的事务性工作单元(基本上是工作单元)。
下面是一个如何一起使用我的工作单元 / 存储库实现的例子:
[WebGet(UriTemplate = "Books")]
public IQueryable<DtoBook> GetBooks()
{
loggerService.Info("Calling IQueryable<DtoBook> GetBooks()");
using (unitOfWork)
{
booksRep.EnrolInUnitOfWork(unitOfWork);
List<Book> books =
booksRep.FindAll().ToList();
IQueryable<DtoBook> dtosBooks = books
.Select(b => DtoTranslator.TranslateToDtoBook(b))
.ToList().AsQueryable<DtoBook>();
return dtosBooks;
}
}
这显然是一个非常简单的例子,但我希望您能明白其中的道理。
log4Net
日志记录在任何应用程序中都至关重要,我们都应该这样做。幸运的是,我们不必自己完成所有繁重的工作,有一些真正优秀的日志框架。在我看来,log4Net是其中的佼佼者。
幸运的是,集成log4Net非常简单。这仅仅是:
假设您已添加log4Net引用,我通常的做法是首先抽象(您知道,这样我们就可以替换为不同的日志库或模拟此库)日志记录,通过一个日志服务,如下所示:
using System;
using log4net;
using Restful.WCFService.Services.Contracts;
namespace Restful.WCFService.Services.Implementation
{
public class Log4NetLoggerService : ILoggerService
{
private ILog logger;
private bool isConfigured = false;
public Log4NetLoggerService()
{
if (!isConfigured)
{
logger = LogManager.GetLogger(typeof(Log4NetLoggerService));
log4net.Config.XmlConfigurator.Configure();
}
}
public void Info(string message)
{
logger.Info(message);
}
public void Warn(string message)
{
logger.Warn(message);
}
public void Debug(string message)
{
logger.Debug(message);
}
public void Error(string message)
{
logger.Error(message);
}
public void Error(Exception ex)
{
logger.Error(ex.Message, ex);
}
public void Fatal(string message)
{
logger.Fatal(message);
}
public void Fatal(Exception ex)
{
logger.Fatal(ex.Message, ex);
}
}
}
然后,我在此基础上,获取日志服务的依赖(这可以在我们之前讨论的MEF部分看到)。
接下来,我添加以下log4Net配置(您可能需要更改此配置或添加新的附加程序)(log4Net最好的功能之一是能够添加新的附加程序,并且log4Net有很多附加程序,并使创建新附加程序的过程非常简单)。
在我的例子中,这在Restful.WCFService的Web.Config中。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
</configSections>
<!-- Log4Net settings -->
<log4net>
<root>
<priority value="Info"/>
<appender-ref ref="RollingFileAppender"/>
</root>
<appender name="RollingFileAppender"
type="log4net.Appender.RollingFileAppender">
<file value="C:\Temp\Restful.WCFService.log" />
<appendToFile value="true" />
<rollingStyle value="Composite" />
<maxSizeRollBackups value="14" />
<maximumFileSize value="15000KB" />
<datePattern value="yyyyMMdd" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern
value="{%level}%date{dd/MM/yyyy HH:mm:ss} - %message%newline"/>
</layout>
</appender>
</log4net>
</configuration>
接下来的事情是确保您让log4Net读取这些设置,您通过运行以下代码行来实现这一点:
XmlConfigurator.Configure();
然后,剩下的就是使用日志服务(下面的代码显示了这一点,其中日志服务由MEF提供):
[WebGet(UriTemplate = "Books")]
public IQueryable<DtoBook> GetBooks()
{
loggerService.Info("Calling IQueryable<DtoBook> GetBooks()");
...
...
...
}
客户端应用程序
正如我所说,对于这个演示解决方案,客户端代码非常简单,它只是一个简单的控制台应用程序。在以后某个时候,当我完成本文的第二部分(这可能永远不会发生)时,我将为其构建一个漂亮的WPF UI。但是,现在,它是一个简单的控制台应用程序,它主要执行以下操作:
- 获取一个
HttpClient
,可用于演示WCF WebApi服务。 - POST一个新
Author
。 - 获取所有
Author
对象。 - 对
Author
进行oData查询。 - POST一个新
Book
。
这是完整的客户端代码,我认为非常简单。最好的部分是,没有针对WCF WebApi服务的特定App.Config内容,我们只需发出请求并处理结果。
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Xml.Serialization;
using Microsoft.ApplicationServer.Http;
using Restful.Models;
using System.Net.Http.Formatting;
using Restful.Models.Dtos;
namespace Restful.Client
{
class Program
{
private enum MimeFormat { JSON, Xml };
string[] randomAuthors = new string[] {
"Jonnie Random",
"Philip Morose",
"Damien Olive",
"Santana Miles",
"Mike Hunt",
"Missy Elliot"
};
string[] randomBooks = new string[] {
"Title1",
"Title2",
"Title3",
"Title4",
"Title5",
"Title6"
};
private Random rand = new Random();
public Program()
{
}
public void SimpleSimulation()
{
string baseUrl = "https://:8300/ef";
//GET Using XML
HttpClient client = GetClient(MimeFormat.Xml);
string uri = "";
//POST Author using XML
DtoAuthor newAuthor =
new DtoAuthor { Name = randomAuthors[rand.Next(randomAuthors.Length)] };
uri = string.Format("{0}/AddAuthor", baseUrl);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, new Uri(uri));
request.Content = new ObjectContent<DtoAuthor>(newAuthor, "application/xml");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
HttpResponseMessage response = client.Send(request);
DtoAuthor receivedAuthor = response.Content.ReadAs<DtoAuthor>();
LogAuthor(uri, receivedAuthor);
//GET All Authors
client = GetClient(MimeFormat.Xml);
uri = string.Format("{0}/Authors", baseUrl);
response = client.Get(uri);
foreach (DtoAuthor author in response.Content.ReadAs<List<DtoAuthor>>())
{
LogAuthor(uri, author);
}
//POST Book using XML
client = GetClient(MimeFormat.Xml);
uri = string.Format("{0}/Authors?$top=1", baseUrl);
response = client.Get(uri);
DtoAuthor authorToUse =
response.Content.ReadAs<List<DtoAuthor>>().FirstOrDefault();
if (authorToUse != null)
{
LogAuthor(uri, authorToUse);
DtoBook newBook = new DtoBook
{
Title = randomBooks[rand.Next(randomBooks.Length)],
AuthorId = authorToUse.Id
};
client = GetClient(MimeFormat.Xml);
uri = string.Format("{0}/AddBook", baseUrl);
request = new HttpRequestMessage(HttpMethod.Post, new Uri(uri));
request.Content = new ObjectContent<DtoBook>(newBook, "application/xml");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
response = client.Send(request);
DtoBook receivedBook = response.Content.ReadAs<DtoBook>();
LogBook(uri, receivedBook);
client = GetClient(MimeFormat.Xml);
uri = string.Format("{0}/Books?$top=1", baseUrl);
response = client.Get(uri);
receivedBook = response.Content.ReadAs<List<DtoBook>>().FirstOrDefault();
LogBook(uri, receivedBook);
}
Console.ReadLine();
}
private void LogAuthor(string action, DtoAuthor author)
{
Console.WriteLine(string.Format("{0}:{1}", action, author));
}
private void LogBook(string action, DtoBook book)
{
Console.WriteLine(string.Format("{0}:{1}", action, book));
}
[STAThread]
static void Main(string[] args)
{
Program p = new Program();
p.SimpleSimulation();
}
private static HttpClient GetClient(MimeFormat format)
{
var client = new HttpClient();
switch (format)
{
case MimeFormat.Xml:
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/xml"));
break;
case MimeFormat.JSON:
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
break;
}
return client;
}
}
}
就是这样
好了,这就是我这次想说的全部内容。我现在要回去完成Pete O'Hanlon和我正在开发的这个OSS项目的最后5%。问题是,最后5%是最难的部分,但我们都投入其中,所以期待它很快就会在这里出现。它已经酝酿了很长时间,但我们都喜欢它,并认为它会有用。在此之前……Dum dum dum。
但是,如果您喜欢这篇文章,并且愿意发表评论/投票,它们总是值得赞赏的。感谢阅读。再见。