.NET领域驱动设计实战系列 专题十一:.NET 领域驱动设计实战系列总结
最后更新于:2022-04-02 00:14:18
# [.NET领域驱动设计实战系列]专题十一:.NET 领域驱动设计实战系列总结
## 一、引用
其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计、领域驱动设计C# 2008实现、领域驱动设计:软件核心复杂性应对之道、实现领域驱动设计和Asp.net 设计模式等书,但是去年的学习仅仅限制于看书,当时看下来感觉,领域驱动设计并没有那么难,并且感觉有些领域驱动设计的内容并没有好的,反而觉得有点华而不实的感觉,所以去年也就放弃了领域驱动设计系列的分享了,但是到今年,在博客园看到还是有很多人写领域驱动的文章,以及介绍了领域驱动设计相关的好处,这时候我就想,领域驱动设计真有这么好吗?但是我并不觉得好了,这时候就想是不是我没有实战没有深刻的感受呢?因此我在今年3月份的时候又重拾领域驱动设计,打算分享一系列关于领域驱动设计实现的文章,所以也就有了这个系列。
## 二、本系列所有专题目录
在刚开始打算写的时候,本以为对领域驱动设计相关理论知识掌握的不错,但当真正打算写的时候,发现之前的知识储备差不多忘的差不多了,无奈下只有重新再拿起书本来温习一遍,不过这次温习很快,因为之前都已经看过一篇。这里分享出来就是想告诉大家,没有真正实践过的东西是很容易忘记的,这时更加坚定了我要写这一系列的文章了。这个初衷也是我一直坚持写这个系列的动力。现在这个系列也告一段落了,从中我确实体会了领域驱动设计的美妙之处,以及现在软件设计的发展和改变。下面是本系列中所有专题的一个目录,为了帮助更好地收藏和自己进行索引,关于实践下来的体会将在下一部分分享给大家。
[[.NET领域驱动设计实战系列]专题一:前期准备之EF CodeFirst](http://www.cnblogs.com/zhili/p/EFCodeFirst.html)
[[.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店](http://www.cnblogs.com/zhili/p/OnlineStorewithDDD.html)
[[.NET领域驱动设计实战系列]专题三:前期准备之规约模式(Specification Pattern)](http://www.cnblogs.com/zhili/p/SpecificationPattern.html)
[[.NET领域驱动设计实战系列]专题四:前期准备之工作单元模式(Unit Of Work)](http://www.cnblogs.com/zhili/p/UnitOfWork.html)
[[.NET领域驱动设计实战系列]专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现](http://www.cnblogs.com/zhili/p/OnlineStore_Second.html)
[[.NET领域驱动设计实战系列]专题六:DDD实践案例:网上书店订单功能的实现](http://www.cnblogs.com/zhili/p/OnlineStoreImplementOrder.html)
[[.NET领域驱动设计实战系列]专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能](http://www.cnblogs.com/zhili/p/OnlineStoreImplementManager.html)
[[.NET领域驱动设计实战系列]专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现](http://www.cnblogs.com/zhili/p/OnlineStoreImplementDistribution.html)
[[.NET领域驱动设计实战系列]专题九:DDD案例:网上书店AOP和站点地图的实现](http://www.cnblogs.com/zhili/p/OnlineStoreImplementAOP.html)
[[.NET领域驱动设计实战系列]专题十:DDD扩展内容:全面剖析CQRS模式实现](http://www.cnblogs.com/zhili/p/CQRSDemo.html)
## 三、总结
通过对领域驱动设计的实践,本人对领域驱动设计的有点和缺点都有了一个清晰的认识。并不是所有软件都适合应用领域驱动来实现的,例如在一些公司还是用三层框架来进行软件的开发,这样并没有什么不好,针对一些业务逻辑简单和后期需求变更不大的软件,完全可以使用三层框架来进行开发,因为三层框架尽管各层之间的依赖关系比较大,不利于扩展。但其好处就是简单,快捷。对于一些小型项目用三层框架是极好的。但对于一些大型项目来说,三层框架可能就不怎么适合了,尤其是大型网站项目。这时候就可以考虑使用领域驱动设计,领域驱动设计推崇的富领域模型,即将相关实体的业务逻辑放在领域实体里面。领域驱动设计思想分层结构更细,使得各层之间的依赖降低,通过引入依赖注入框架拉进入达到低耦合,高内聚原则。并且通过仓储模式,可以使得针对其他数据库的存储也可以很方便的进行扩展。采用领域驱动设计也可以更多实施测试驱动开发,早在以前的项目,哪里会有单元测试这个东西啊。
通过这个系列最深刻的感受,除了对领域驱动设计有了更进一步的认识外,还有一点更深刻的感受就是做软件的一定要把自己学到的内容实践起来,并且通过博文或其他方式进行总结,这样才能更好的积累。尽管通过博文的方式不经常用一样会忘记,但是很多东西你总结了就是和没总结的不一样,总结了可以对知识有一个系统的梳理,这样可以让你深刻理解知识点,尽管忘记了,它也是被记录在大脑的某个角度,当重新遇到问题时,你完全可以通过自己写的博文重新找回来,并且找回来的认识并不会比之前的理解少,可能更加多,但是不总结的话,那种忘记可能就是真的忘记了,等于没看一样。所以,对于做软件来说,真需要多实践。所以,还是奉劝大家可以多总结,多实践,抛下浮躁的心态,想做好技术,需要的静下心来专研和实践。最近,刚接触的一个项目用到了一个一些非关系数据的内容。所以接下来,我将会新开一个非关系数据库的系列来进行总结自己这段时间里的经历。其中包括Mongodb、Redis等非关系数据库的相关内容。
最后附上,所有专题的完整DDD实践案例下载地址:
**DDD实践案例下载地址:[DDD实践案例:网上书店](https://github.com/lizhi5753186/OnlineStore)**
';
.NET领域驱动设计实战系列 专题十:DDD扩展内容:全面剖析CQRS模式实现
最后更新于:2022-04-02 00:14:15
# [.NET领域驱动设计实战系列]专题十:DDD扩展内容:全面剖析CQRS模式实现
## 一、引言
前面介绍的所有专题都是基于经典的领域驱动实现的,然而,领域驱动除了经典的实现外,还可以基于CQRS模式来进行实现。本专题将全面剖析如何基于CQRS模式(Command Query Responsibility Segregation,命令查询职责分离)来实现领域驱动设计。
## 二、CQRS是什么?
在介绍具体的实现之前,对于之前不了解CQRS的朋友来说,首先第一个问题应该是:什么是CQRS啊?你倒是详细介绍完CQRS后再介绍具体实现啊?既然大家会有这样的问题,所以本专题首先全面介绍下什么是CQRS。
## 2.1 CQRS发展历程
在介绍CQRS之前,我觉得有必要先了解一下CQS(即Command Query Separation,命令查询分离)模式。我们可以理解CQRS是在DDD的实践中基于CQS理论而出现的一种体系结构模式。CQS模式最早由软件大师Bertrand Meyer(Eiffel语言之父,面向对象开-闭原则OCP提出者)提出,他认为,对象的行为仅有两种:命令和查询,不存在第三种情况。根据CQS的思想,任何方法都可以拆分为命令和查询两部分。例如下面的方法:
在上面的方法中,执行了一个命令,即对变量_number加上一个因子factor,同时又执行了一个查询,即查询返回_number的值。根据CQS的思想,该方法可以拆成Command和Query两个方法:
```
private int _number = 0;
private void AddCommand(int factor)
{
_number += factor;
}
private int QueryValue()
{
return _number;
}
```
命令和查询分离使得我们可以更好地把握对象的细节,更好地理解哪些操作会改变系统的状态。从而使的系统具有更好的扩展性,并获得更好的性能。
CQRS根据CQS思想,并结合领域驱动设计思想,由Grey Young在[CQRS, Task Based UIs, Event Sourcing agh!](http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/) 这篇文章中提出。**CQRS将之前只需要定义一个对象拆分成两个对象,分离的原则按照对象中方法是执行命令还是执行查询来进行拆分的。**
## **2.2 CQRS结构**
由前面的介绍可知,采用CQRS模式实现的系统结构可以分为两个部分:命令部分和查询部分。其系统结构如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4bc4112.jpg)
从上面系统结构图可以发现,采用CQRS实现的领域驱动设计与经典DDD有很大的不同。采用CQRS实现的DDD结构大体分为两部分,查询部分和命令部分,并且维护着两个数据库实例,一个专门用来进行查询,另一个用来响应命令操作。然后通过EventHandler操作将命令改变的状态同步到用来查询的数据库实例中。从这个描述中,我们可能会联想到数据库级别主从读写分离。然而数据读写分离是在数据库层面来实现读写分离的机制,而CQRS是在业务逻辑层面来实现读写分离机制。两者是站在两个不同的层面对读写分离进行实现的。
## 三、为什么需要引入CQRS模式
前面我们已经详细介绍了CQRS模式,相信经过前面的介绍,大家对CQRS模式一定有一些了解了,但为什么要引入CQRS模式呢?
在传统的实现中,对DB执行增、删、改、查所有操作都会放在对应的仓储中,并且这些操作都公用一份领域实体对象。对于一些简单的系统,使用传统的设计方式并没有什么不妥,但在一些大型复杂的系统中,传统的实现方式也会存在一些问题:
* 使用同一个领域实体来进行数据读写可能会遇到资源竞争的情况。所以经常要处理锁的问题,在写入数据的时候,需要加锁,读取数据的时候需要判断是否允许脏读。这样使得系统的逻辑性和复杂性增加,并会影响系统的吞吐量。
* 在大数据量同时进行读写的情况下,可能出现性能的瓶颈。
* 使用同一个领域实体来进行数据库读写可能会太粗糙。在大多是情况下,比如编辑操作,可能只需要更新个别字段,这时却需要将整个对象都穿进去。还有在查询的时候,表现层可能只需要个别字段,但需要查询和返回整个领域实体,再把领域实体对象转换从对应的DTO对象。
* 读写操作都耦合在一起,不利于对问题的跟踪和分析,如果读写操作分离的话,如果是由于状态改变的问题就只需要去分析写操作相关的逻辑就可以了,如果是关于数据的不正确,则只需要关心查询操作的相关逻辑即可。
针对上面的这些问题,采用CQRS模式的系统都可以解决。由于CQRS模式中将查询和命令进行分析,所以使得两者分工明确,各自负责不同的部分,并且在业务上将命令和查询分离能够提高系统的性能和可扩展性。既然CQRS这么好,那是不是所有系统都应该基于CQRS模式去实现呢?显然不是的,CQRS也有其使用场景:
1. 系统的业务逻辑比较复杂的情况下。因为本来业务逻辑就比较复杂了,如果再把命令操作和查询操作绑定同一个业务实体的话,这样会导致后期的需求变更难于进行扩展下去。
2. 需要对系统中查询性能和写入性能分开进行优化的情况下,尤其读/写比例非常高的情况下。例如,在很多系统中读操作的请求数远大于写操作,此时,就可以考虑将写操作抽离出来进行单独扩展。
3. 系统在将来随着时间不断变化的情况下。
然而,CQRS也有其不适用的场景:
* 业务逻辑比较简单的情况下,此时采用CQRS反而会把系统搞的复杂。
* 系统用户访问量都比较小的情况下,并且需求以后不怎么会变更的情况下。针对这样的系统,完全可以用传统的实现方式快速将系统实现出来,没必要引入CQRS来增加系统的复杂度。
## 四、事件溯源
**在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回,这个方面的操作相对比较简单。而命令方面,是通过发送具体Command,接着由CommandBus来分发到具体的CommandHandle来进行处理,CommandHandle在进行处理时,并没有直接将对象的状态保存到外部持久化结构中,而仅仅是从领域对象中获得产生的一系列领域事件,并将这些事件保存到Event Store中,同时将事件发布到事件总线Event Bus进行下一步处理;接着Event Bus同样进行协调,将具体的事件交给具体的Event Handle进行处理,最后Event Handler再把对象的状态保存到对应Query数据库中。**
上面过程正是CQRS系统中的调用顺序。从中可以发现,采用CQRS实现的系统存在两个数据库实例,一个是Event Store,该数据库实例用来保存领域对象中发生的一系列的领域事件,简单来说就是保存领域事件的数据库。另一个是Query Database,该数据库就是存储具体的领域对象数据的,查询操作可以直接对该数据库进行查询。由于,我们在Event Store中记录领域对象发生的所有事件,这样我们就可以通过查询该数据库实例来获得领域对象之前的所有状态了。所谓Event Sourcing,就是指的的是:通过事件追溯对象的起源,它允许通过记录下来的事件,将领域模型恢复到之前的任意一个时间点。
通过Event来记录领域对象所发生的所有状态,这样利用系统的跟踪并能够方便地回滚到某一历史状态。经过上面的描述,感觉事件溯源一般用于系统的维护。例如,我们可以设计一个同步服务,该服务程序从Event Store数据库查询出领域对象的历史数据,从而打印生成一个历史报表,如历史价格报表等。但正是的CQRS系统中如何使用Event Sourcing的呢?
在前面介绍CQRS系统的调用顺序中,我们讲到,由Event Handler将对象的状态保存到对应的Query数据库中,这里有一个问题,对象的状态怎么获得呢?对象状态的获得正是由Event sourcing机制来获得,因为用户发送的仅仅是Command,Command中并不包含对象的状态数据,所以此时需要通过Event Sourcing机制来查询Event Store来还原对象的状态,还原根据就是对应的Id,该Id是通过命令传入的。**Event Sourcing的调用需要放在CommandHandle中,因为CommandHandle需要先获得领域对象,这样才能把领域对象与命令对象来进行对比,从而获得领域对象中产生的一系列领域事件。**
## 五、快照
然而,当随着时间的推移,领域事件变得越来越多时,通过Event Sourcing机制来还原对象状态的过程会非常耗时,因为每一次都需要从最早发生的事件开始。那有没有好的一个方式来解决这个问题呢?答案是肯定的,即在Event Sourcing中引入快照(Snapshots)实现。实现原理就是——没产生N个领域事件,则对对象做一次快照。这样,**领域对象溯源的时候,可以先从快照中获得最近一次的快照,然后再逐个应用快照之后所有产生的领域事件,而不需要每次溯源都从最开始的事件开始对对象重建,这样就大大加快了对象重建的过程。**
## 六、CQRS模式实现和剖析
前面介绍了那么多CQRS的内容,下面就具体通过一个例子来演示下CQRS系统的实现。
命令部分的实现
```
// 应用程序初始化操作,将依赖的对象通过依赖注入框架StructureMap进行注入
public sealed class ServiceLocator
{
private static readonly ICommandBus _commandBus;
private static readonly IStorage _queryStorage;
private static readonly bool IsInitialized;
private static readonly object LockThis = new object();
static ServiceLocator()
{
if (!IsInitialized)
{
lock (LockThis)
{
// 依赖注入
ContainerBootstrapper.BootstrapStructureMap();
_commandBus = ContainerBootstrapper.Container.GetInstance();
_queryStorage = ContainerBootstrapper.Container.GetInstance();
IsInitialized = true;
}
}
}
public static ICommandBus CommandBus
{
get { return _commandBus; }
}
public static IStorage QueryStorage
{
get { return _queryStorage; }
}
}
class ContainerBootstrapper
{
private static Container _container;
public static void BootstrapStructureMap()
{
_container = new Container(x =>
{
x.For(typeof (IDomainRepository<>)).Singleton().Use(typeof (DomainRepository<>));
x.For().Singleton().Use();
x.For().Use();
x.For().Use();
x.For().Use();
x.For().Use();
x.For().Use();
});
}
public static Container Container
{
get { return _container;}
}
}
public class HomeController : Controller
{
[HttpPost]
public ActionResult Add(DiaryItemDto item)
{
// 发布CreateItemCommand到CommandBus中
ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To));
return RedirectToAction("Index");
}
}
// CommandBus 的实现
public class CommandBus : ICommandBus
{
private readonly ICommandHandlerFactory _commandHandlerFactory;
public CommandBus(ICommandHandlerFactory commandHandlerFactory)
{
_commandHandlerFactory = commandHandlerFactory;
}
public void Send(T command) where T : Command
{
// 获得对应的CommandHandle来对命令进行处理
var handlers = _commandHandlerFactory.GetHandlers();
foreach (var handler in handlers)
{
// 处理命令
handler.Execute(command);
}
}
}
// 对CreateItemCommand处理类
public class CreateItemCommandHandler : ICommandHandler
{
private readonly IDomainRepository _domainRepository;
public CreateItemCommandHandler(IDomainRepository domainRepository)
{
_domainRepository = domainRepository;
}
// 具体处理逻辑
public void Execute(CreateItemCommand command)
{
if (command == null)
{
throw new ArgumentNullException("command");
}
if (_domainRepository == null)
{
throw new InvalidOperationException("domainRepository is not initialized.");
}
var aggregate = new DiaryItem(command.ID, command.Title, command.Description, command.From, command.To)
{
Version = -1
};
// 将对应的领域实体进行保存
_domainRepository.Save(aggregate, aggregate.Version);
}
}
// IDomainRepository的实现类
public class DomainRepository : IDomainRepository where T : AggregateRoot, new()
{
// 并没有直接对领域实体进行保存,而是先保存领域事件进EventStore,然后在Publish事件到EventBus进行处理
// 然后EventBus把事件分配给对应的事件处理器进行处理,由事件处理器来把领域对象保存到QueryDatabase中
public void Save(AggregateRoot aggregate, int expectedVersion)
{
if (aggregate.GetUncommittedChanges().Any())
{
_storage.Save(aggregate);
}
}
}
// Event Store的实现,这里保存在内存中,通常是保存到具体的数据库中,如SQL Server、Mongodb等
public class InMemoryEventStorage : IEventStorage
{
// 领域事件的保存
public void Save(AggregateRoot aggregate)
{
// 获得对应领域实体未提交的事件
var uncommittedChanges = aggregate.GetUncommittedChanges();
var version = aggregate.Version;
foreach (var @event in uncommittedChanges)
{
version++;
// 没3个事件创建一次快照
if (version > 2)
{
if (version % 3 == 0)
{
var originator = (ISnapshotOrignator)aggregate;
var snapshot = originator.CreateSnapshot();
snapshot.Version = version;
SaveSnapshot(snapshot);
}
}
@event.Version = version;
// 保存事件到EventStore中
_events.Add(@event);
}
// 保存事件完成之后,再将该事件发布到EventBus 做进一步处理
foreach (var @event in uncommittedChanges)
{
var desEvent = TypeConverter.ChangeTo(@event, @event.GetType());
_eventBus.Publish(desEvent);
}
}
}
// EventBus的实现
public class EventBus : IEventBus
{
private readonly IEventHandlerFactory _eventHandlerFactory;
public EventBus(IEventHandlerFactory eventHandlerFactory)
{
_eventHandlerFactory = eventHandlerFactory;
}
public void Publish(T @event) where T : DomainEvent
{
// 获得对应的EventHandle来处理事件
var handlers = _eventHandlerFactory.GetHandlers();
foreach (var eventHandler in handlers)
{
// 对事件进行处理
eventHandler.Handle(@event);
}
}
}
// DiaryItemCreatedEvent的事件处理类
public class DiaryIteamCreatedEventHandler : IEventHandler
{
private readonly IStorage _storage;
public DiaryIteamCreatedEventHandler(IStorage storage)
{
_storage = storage;
}
public void Handle(DiaryItemCreatedEvent @event)
{
var item = new DiaryItemDto()
{
Id = @event.SourceId,
Description = @event.Description,
From = @event.From,
Title = @event.Title,
To = @event.To,
Version = @event.Version
};
// 将领域对象持久化到QueryDatabase中
_storage.Add(item);
}
}
```
上面代码主要演示了Command部分的实现,从代码可以看出,首先我们需要通过ServiceLocator类来对依赖注入对象进行注入,然后UI层通过CommandBus把对应的命令发布到CommandBus中进行处理,命令总线再查找对应的CommandHandler来对命令进行处理,接着CommandHandler调用仓储类来保存领域对象对应的事件,保存事件成功后再将事件发布到事件总线中进行处理,然后由对应的事件处理程序将领域对象保存到QueryDatabase中。这样就完成了命令部分的操作,从中可以发现,命令部分的实现和CQRS系统中的系统结构图的处理过程是一样的。然而创建日志命令并没有涉及事件溯源操作,因为创建命令并需要重建领域对象,此时的领域对象是通过创建日志命令来获得的,但在修改和删除命令中涉及了事件溯源,因为此时需要根据命令对象的ID来重建领域对象。具体的实现可以参考源码。
下面让我们再看看查询部分的实现。
查询部分的实现代码:
```
public class HomeController : Controller
{
// 查询部分
public ActionResult Index()
{
// 直接获得QueryDatabase对象来查询所有日志
var model = ServiceLocator.QueryStorage.GetItems();
return View(model);
}
}
public class InMemoryStorage : IStorage
{
private static readonly List Items = new List();
public DiaryItemDto GetById(Guid id)
{
return Items.FirstOrDefault(a => a.Id == id);
}
public void Add(DiaryItemDto item)
{
Items.Add(item);
}
public void Delete(Guid id)
{
Items.RemoveAll(i => i.Id == id);
}
public List GetItems()
{
return Items;
}
}
```
从上面代码可以看出,查询部分的代码实现相对比较简单,UI层直接通过QueryDatabase来查询领域对象,然后由UI层进行渲染出来显示。
到此,一个简单的CQRS系统就完成了,然而在项目中,UI层并不会直接CommandBus和QueryDatabase进行引用,而是通过对应的CommandService和QueryService来进行协调,具体的系统结构如下图所示(只是在CommandBus和Query Database前加入了一个SOA的服务层来进行协调,这样有利于系统扩展,可以通过SOA服务来进行请求路由,将不同请求路由不同的系统中,这样会可以实现多个系统进行一个整合):
![](http://images0.cnblogs.com/blog2015/383187/201506/211820315297341.png)
关于该CQRS系统的演示效果,大家可以自行去Github或MSDN中进行下载,具体的下载地址将会本专题最后给出。
## 七、总结
到这里,本专题关于CQRS的介绍就结束了,并且本专题也是领域驱动设计系列的最后一篇了。本系列专题的内容主要是参考daxnet的ByteartRetail案例,由于daxnet在写这个案例的时候并没有一步一步介绍其创建过程,对于一些领域驱动的初学者来说,直接去学习这个案例未免会有点困难,导致学习兴趣降低,从而放弃领域驱动的学习。为了解决这些问题,所以,本人对ByteartRetail案例进行剖析,并参考该案例一步步实现自己的领域驱动案例OnlineStore。希望本系列可以帮助大家打开领域驱动的大门。
由于现在NO-SQL在互联网行业的应用已经非常流行,以至于面试的时候经常会被问到你用过的非关系数据库有哪些?所以本人也不想Out,所以在最近2个月的时候学习了一些No-SQL的内容,所以,接下来,我将会开启一个NO-SQL系列,记录自己这段时间来学习NO-SQL的一些心得和体会。
本专题所有源码下载:
Github地址:[https://github.com/lizhi5753186/CQRSDemo](https://github.com/lizhi5753186/CQRSDemo)
MSDN地址:[https://code.msdn.microsoft.com/CQRS-1f05ebe5](https://code.msdn.microsoft.com/CQRS-1f05ebe5)
本文参考链接:
[http://www.codeproject.com/Articles/555855/Introduction-to-CQRS](http://www.codeproject.com/Articles/555855/Introduction-to-CQRS%20)
[http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html](http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html)
';
.NET领域驱动设计实战系列 专题九:DDD案例:网上书店AOP和站点地图的实现
最后更新于:2022-04-02 00:14:13
# [.NET领域驱动设计实战系列]专题九:DDD案例:网上书店AOP和站点地图的实现
## 一、引言
在前面一专题介绍到,要让缓存生效还需要实现对AOP(面向切面编程)的支持。所以本专题将介绍了网上书店案例中AOP的实现。关于AOP的概念,大家可以参考文章:[http://www.cnblogs.com/jin-yuan/p/3811077.html](http://www.cnblogs.com/jin-yuan/p/3811077.html)。这里我简单介绍下AOP:AOP可以理解为对方法进行截获,这样就可以在方法调用前或调用后插入需要的逻辑。例如可以在方法调用前,加入缓存查找逻辑等。这里缓存查找逻辑就在方法调用前被执行。通过对AOP的支持,每个方法就可以分为3部分了,方法调用前逻辑->具体需要调用的方法->方法调用后的逻辑。也就是在方法调用的时候“切了一刀”。
## 二、网上书店AOP的实现
你可以从零开始去实现AOP,但是目前已经存在很多AOP框架了,所以在本案例中将直接通过Unity的AOP框架(Unity.Interception)来实现网上书店对AOP的支持。通常AOP的实现放在基础设施层进行实现,因为可能其他所有层都需要加入对AOP的支持。本案例中将对两个方面的AOP进行实现,一个是方法调用前缓存的记录或查找,另一个是方法调用后异常信息的记录。在实现具体代码之前,我们需要在基础设施层通过Nuget来引入Unity.Interception包。添加成功之后,我们需要定义两个类分别去实现AOP框架中**IInterceptionBehavior**接口。由于本案例中需要对缓存和异常日志功能进行AOP实现,自然就需要定义CachingBehavior和ExceptionLoggingBehavior两个类去实现**IInterceptionBehavior**接口。首先让我们看看CachingBehavior类的实现,具体实现代码如下所示:
```
// 缓存AOP的实现
public class CachingBehavior : IInterceptionBehavior
{
private readonly ICacheProvider _cacheProvider;
public CachingBehavior()
{
_cacheProvider = ServiceLocator.Instance.GetService();
}
// 生成缓存值的键值
private string GetValueKey(CacheAttribute cachingAttribute, IMethodInvocation input)
{
switch (cachingAttribute.Method)
{
// 如果是Remove,则不存在特定值键名,所有的以该方法名称相关的缓存都需要清除
case CachingMethod.Remove:
return null;
// 如果是Get或者Update,则需要产生一个针对特定参数值的键名
case CachingMethod.Get:
case CachingMethod.Update:
if (input.Arguments != null &&
input.Arguments.Count > 0)
{
var sb = new StringBuilder();
for (var i = 0; i < input.Arguments.Count; i++)
{
sb.Append(input.Arguments[i]);
if (i != input.Arguments.Count - 1)
sb.Append("_");
}
return sb.ToString();
}
else
return "NULL";
default:
throw new InvalidOperationException("无效的缓存方式。");
}
}
#region IInterceptionBehavior Members
public IEnumerable GetRequiredInterfaces()
{
return Type.EmptyTypes;
}
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
// 获得被拦截的方法
var method = input.MethodBase;
var key = method.Name; // 获得拦截的方法名
// 如果拦截的方法定义了Cache属性,说明需要对该方法的结果需要进行缓存
if (!method.IsDefined(typeof (CacheAttribute), false))
return getNext().Invoke(input, getNext);
var cachingAttribute = (CacheAttribute)method.GetCustomAttributes(typeof (CacheAttribute), false)[0];
var valueKey = GetValueKey(cachingAttribute, input);
switch (cachingAttribute.Method)
{
case CachingMethod.Get:
try
{
// 如果缓存中存在该键值的缓存,则直接返回缓存中的结果退出
if (_cacheProvider.Exists(key, valueKey))
{
var value = _cacheProvider.Get(key, valueKey);
var arguments = new object[input.Arguments.Count];
input.Arguments.CopyTo(arguments, 0);
return new VirtualMethodReturn(input, value, arguments);
}
else // 否则先调用方法,再把返回结果进行缓存
{
var methodReturn = getNext().Invoke(input, getNext);
_cacheProvider.Add(key, valueKey, methodReturn.ReturnValue);
return methodReturn;
}
}
catch (Exception ex)
{
return new VirtualMethodReturn(input, ex);
}
case CachingMethod.Update:
try
{
var methodReturn = getNext().Invoke(input, getNext);
if (_cacheProvider.Exists(key))
{
if (cachingAttribute.IsForce)
{
_cacheProvider.Remove(key);
_cacheProvider.Add(key, valueKey, methodReturn.ReturnValue);
}
else
_cacheProvider.Update(key, valueKey, methodReturn);
}
else
_cacheProvider.Add(key, valueKey, methodReturn.ReturnValue);
return methodReturn;
}
catch (Exception ex)
{
return new VirtualMethodReturn(input, ex);
}
case CachingMethod.Remove:
try
{
var removeKeys = cachingAttribute.CorrespondingMethodNames;
foreach (var removeKey in removeKeys)
{
if (_cacheProvider.Exists(removeKey))
_cacheProvider.Remove(removeKey);
}
**// 执行具体截获的方法** var methodReturn = getNext().Invoke(input, getNext);
return methodReturn;
}
catch (Exception ex)
{
return new VirtualMethodReturn(input, ex);
}
default: break;
}
return getNext().Invoke(input, getNext);
}
public bool WillExecute
{
get { return true; }
}
#endregion
}
```
从上面代码可以看出,通过Unity.Interception框架来实现AOP变得非常简单了,我们只需要实现IInterceptionBehavior接口中的Invoke方法和WillExecute属性即可。并且从上面代码可以看出,AOP的支持最核心代码实现在于Invoke方法的实现。既然我们需要在方法调用前查找缓存,如果缓存不存在再调用方法从数据库中进行查找,如果存在则直接从缓存中进行读取数据即可。自然需要在 getNext().Invoke(input, getNext)代码执行前进缓存进行查找,然而上面CachingBehavior类正式这样实现的。
介绍完缓存功能AOP的实现之后,下面具体看看异常日志的AOP实现。具体实现代码如下所示:
```
// 用于异常日志记录的拦截行为
public class ExceptionLoggingBehavior :IInterceptionBehavior
{
///
/// 需要拦截的对象类型的接口
///
///
public IEnumerable GetRequiredInterfaces()
{
return Type.EmptyTypes;
}
///
/// 通过该方法来拦截调用并执行所需要的拦截行为
///
/// 调用拦截目标时的输入信息
/// 通过行为链来获取下一个拦截行为的委托
/// 从拦截目标获得的返回信息
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
// 执行目标方法
var methodReturn = getNext().Invoke(input, getNext);
// 方法执行后的处理
if (methodReturn.Exception != null)
{
Utils.Log(methodReturn.Exception);
}
return methodReturn;
}
// 表示当拦截行为被调用时,是否需要执行某些操作
public bool WillExecute
{
get { return true; }
}
}
```
异常日志功能的AOP实现与缓存功能的AOP实现类似,只是一个需要在方法执行前注入,而一个是在方法执行后进行注入罢了,其实现原理都是在截获的方法前后进行。方法截获功能AOP框架已经帮我们实现了。
到此,我们网上书店AOP的实现就已经完成了,但要正式生效还需要通过配置文件把AOP的实现注入到需要截获的方法当中去,这样执行这些方法才会执行注入的行为。对应的配置文件如下标红部分所示:
```
**
**
```
到此,网上书店案例中AOP的实现就完成了。通过上面的配置可以看出,客户端在调用应用服务方法前后会调用我们注入的行为,即缓存行为和异常日志行为。通过对AOP功能的支持,就不需要为每个需要进行缓存或需要异常日志行为的方法来重复写这些相同的逻辑了。从而避免了重复代码的重复实现,提高了代码的重用性和降低了模块之间的依赖性。
## 三、网上书店案例中站点地图的实现
在大部分网站中都实现了站点地图的功能,在Asp.net中,我们可以通过SiteMap模块来实现站点地图的功能,在Asp.net MVC也可以通过MvcSiteMapProvider第三方开源框架来实现站点地图。所以针对网上书店案例,站点地图的支持也是必不可少的。下面让我们具体看看站点地图在本案例中是如何去实现的呢?
在看实现代码之前,让我们先来理清下实现思路。
本案例中站点地图的实现,并没有借助MvcSiteMapProvider第三方框架来实现。其实现原理首先获得用户的路由请求,然后根据用户请求根据站点地图的配置获得对应的配置节点,接着根据站点地图的节点信息生成类似">首页>"这样带标签的字符串;如果获得的节点是配置文件中某个父节点的子节点,此时会通过递归的方式找到其父节点,然后递归地生成对应带标签的字符串,从而完成站点地图的功能。分析完实现思路之后,下面让我们再对照下具体的实现代码来加深理解。具体的实现代码如下所示:
```
public class MvcSiteMap
{
private static readonly MvcSiteMap _instance = new MvcSiteMap();
private static readonly XDocument Doc = XDocument.Load(HttpContext.Current.Server.MapPath(@"~/SiteMap.xml"));
private UrlHelper _url = null;
private string _currentUrl;
public static MvcSiteMap Instance
{
get { return _instance;}
}
private MvcSiteMap()
{
}
public MvcHtmlString Navigator()
{
// 获得当前请求的路由信息
_url = new UrlHelper(HttpContext.Current.Request.RequestContext);
var routeUrl = _url.RouteUrl(HttpContext.Current.Request.RequestContext.RouteData.Values);
if (routeUrl != null)
_currentUrl = routeUrl.ToLower();
// 从配置的站点Xml文件中找到当前请求的Url相同的节点
var c = FindNode(Doc.Root);
var temp = GetPath(c);
return MvcHtmlString.Create(BuildPathString(temp));
}
// 从SitMap配置文件中找到当前请求匹配的节点
private XElement FindNode(XElement node)
{
// 如果xml节点对应的url是否与当前请求的节点相同,如果相同则直接返回xml对应的节点
// 如果不同开始递归子节点
return IsUrlEqual(node) == true ? node : RecursiveNode(node);
}
// 判断xml节点对应的url是否与当前请求的url一样
private bool IsUrlEqual(XElement c)
{
var a = GetNodeUrl(c).ToLower();
return a == _currentUrl;
}
// 递归Xml节点
private XElement RecursiveNode(XElement node)
{
foreach (var c in node.Elements())
{
if (IsUrlEqual(c) == true)
{
return c;
}
else
{
var x = RecursiveNode(c);
if (x != null)
{
return x;
}
}
}
return null;
}
// 获得xml节点对应的请求url
private string GetNodeUrl(XElement c)
{
return _url.Action(c.Attribute("action").Value, c.Attribute("controller").Value,
new {area = c.Attribute("area").Value});
}
// 根据对应请求url对应的Xml节点获得其在Xml中的路径,即获得其父节点有什么
// SiteMap.xml 中节点的父子节点一定要配置对
private Stack GetPath(XElement c)
{
var temp = new Stack();
while (c != null)
{
temp.Push(c);
c = c.Parent;
}
return temp;
}
// 根据节点的路径来拼接带标签的字符串
private string BuildPathString(Stack m)
{
var sb = new StringBuilder();
var tc = new TagBuilder("span");
tc.SetInnerText(">");
var sp = tc.ToString();
var count = m.Count;
for (var x = 1; x <= count; x++)
{
var c = m.Pop();
TagBuilder tb;
if (x == count)
{
tb = new TagBuilder("span");
}
else
{
tb = new TagBuilder("a");
tb.MergeAttribute("href", GetNodeUrl(c));
}
tb.SetInnerText(c.Attribute("title").Value);
sb.Append(tb);
sb.Append(sp);
}
return sb.ToString();
}
}
```
对应的站点地图配置信息如下所示:
```
```
实现完成之后,下面让我们具体看看本案例中站点地图的实现效果看看,具体运行效果如下图所示:
![](http://images0.cnblogs.com/blog2015/383187/201506/192128197483378.png)
## 四、小结
到这里,本专题的内容就结束了。本专题主要借助Unity.Interception框架在网上书店中引入了AOP功能,并且最后简单介绍了站点地图的实现。在下一专题将对CQRS模式做一个全面的介绍。
本案例所有源码:[https://github.com/lizhi5753186/OnlineStore_Second/](https://github.com/lizhi5753186/OnlineStore_Second/)
';
.NET领域驱动设计实战系列 专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现
最后更新于:2022-04-02 00:14:11
# [.NET领域驱动设计实战系列]专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现
## 一、引言
在上一专题中,商家发货和用户确认收货功能引入了消息队列来实现的,引入消息队列的好处可以保证消息的顺序处理,并且具有良好的可扩展性。但是上一专题消息队列是基于内存中队列对象来实现,这样实现有一个弊端,就是一旦服务重启或出现故障时,此时消息队列中的消息会丢失,并且也记录不了日志。所以就会出现,商家发货成功后,用户并没有收到邮件通知,并且也没有日志让我们发现是否发送了邮件通知。为了解决这个问题,就需要引入一种可恢复的消息队列。目前有很多开源的消息队列都支持可恢复的,例如TibcoEms.net等。然而,微软的MSMQ也是支持这种特性的。并且MSMQ还支持分布式部署,关于MSMQ更多内容可以参考:[http://www.cnblogs.com/zhili/p/MSMQ.html](http://www.cnblogs.com/zhili/p/MSMQ.html)
在本专题中将介绍为网上书店案例引入分布式消息队列和分布式缓存的实现。
## 二、分布式消息队列的实现
MSMQ的实现原理是:消息的发送者把自己想要发送的信息放入一个容器,然后把它保存到一个系统公用空间的消息队列中,本地或异地的消息接收程序再从该队列中取出发给它的消息进行处理。所以,即使服务器突然重启,消息也会存在于系统公用空间的消息队列中,待服务器重新启动后,可以继续接受消息进行处理,从而解决上一专题存在的问题。另外,上一专题的消息队列只能被用在当前服务器中,而MSMQ支持分布式部署,不同机器都可以对MSMQ进行接收消息来处理,此时MSMQ起到一个中间件的作用。
在为网上书店引入分布式消息队列之前,让我们先理一下实现思路:
* 上一专题中把发货事件和收货事件发布到EventBus中,而此时需要用MsmqEventBus来替代EventBus。而MsmqEventBus的实现就很简单了,完全可以参考EventBus来实现,只是此时消息并不是进入Queue对象中,而是通过[MessageQueue](https://msdn.microsoft.com/zh-cn/library/System.Messaging.MessageQueue(v=vs.110).aspx%20)对象发送到系统的消息队列中。
* 而Commit方法即从系统的消息队列中出队来获得消息。再获得消息的处理器时,与上一专题的实现有点不同,因为把事件对象发送到消息队列时,需要先把事件对象先序列化为Message对象再放入消息队列中,而出队的也是消息对象,而不是上一专题中的发货事件对象。所以此时需要把出队的消息对象反序列化为对应的事件对象。
有了上面的实现思路,接下来让我们一起看看MsmqEventBus的具体实现代码吧。
```
public class MsmqEventBus : DisposableObject, IEventBus
{
public void Publish(TMessage message) where TMessage : class, IEvent
{
// 将消息放入Message中Body属性进行序列化发送到消息队列中
var msmqMessage = new Message(message) { Formatter = new XmlMessageFormatter(new[] { message.GetType() }), Label = message.GetType().ToString()};
_messageQueue.Send(msmqMessage);
_committed = false;
}
public void Publish(IEnumerable messages) where TMessage : class, IEvent
{
messages.ToList().ForEach(m =>
{
_messageQueue.Send(m);
_committed = false;
});
}
public void Commit()
{
if (this._useInternalTransaction)
{
using (var transaction = new MessageQueueTransaction())
{
try
{
transaction.Begin();
var message = _messageQueue.Receive();
if (message != null)
{
message.Formatter = new XmlMessageFormatter(new[] { typeof(string) });
var evntType = ConvertStringToType(message.Body.ToString());
var method = _publishMethod.MakeGenericMethod(evntType);
var evnt = Activator.CreateInstance(evntType);
method.Invoke(_aggregator, new object[] { evnt });
transaction.Commit();
}
}
catch
{
transaction.Abort();
throw;
}
}
}
else
{
// 从msmq消息队列中出队,此时获得的对象是消息对象
var message = _messageQueue.Receive();
if (message != null)
{
// 指定反序列化的对象,由于我们之前把对应的事件类型保存在MessageQueue中的Label属性
// 所以此时可以通过Label属性来获得目标序列化类型
message.Formatter = new XmlMessageFormatter(new[] { ConvertStringToType(message.Label) });
// 这样message.Body获得就是对应的事件对象,后面的处理逻辑就和EventBus一样了
var evntType =message.Body.GetType();
var method = _publishMethod.MakeGenericMethod(evntType);
method.Invoke(_aggregator, new object[] { message.Body });
}
}
_committed = true;
}
}
```
结合上面代码的注释和前面实现思路的介绍,相信理解MsmqEventBus应该没什么问题了。接下来,我们需要在配置文件中指定EventBus为MsmqEventBus类,另外**需要在你本地专有队列中创建"OnlineStoreQueue"队列来接受消息**。具体的配置文件修改为:
```
```
到此,分布式消息队列的实现就完成了,具体分布式消息队列的实现效果和上一专题使用EventBus的效果是一样的,这里就不再贴图了,大家可以自行下载源码查看。
## 三、缓存的实现
在实际开发过程中,缓存的实现是必不可少的,对于已经查询过的数据可以直接从缓存中进行读取返回给调用者,利用缓存不但可以加快响应速度,还能减轻数据库服务器的压力。在大型电子商务网站中,缓存的实现更是必不可少的功能。然而缓存的实现也有两种,一种是分布式缓存,另一种本地缓存。在大型网站中,更多实现的是分布式缓存,对于一些少用户的企业系统,可能才会使用到本地缓存。所以在本专题中,将在网上书店案例中对这两种缓存分别进行实现。
## 3.1 本地缓存的实现
首先,我们来介绍本地缓存的实现。由于这里需要实现两种缓存,根据面向接口编程原则,我们自然首先需要定义一个缓存接口,然后这两种具体缓存都需要实现该接口。针对缓存接口,无非是缓存数据的添加,移除,更新等操作,所以缓存接口的定义如下所示:
```
// 缓存接口的定义
public interface ICacheProvider
{
///
/// 向缓存中添加一个对象
///
/// 缓存的键值
/// 缓存值的键值
/// 缓存的对象
void Add(string key, string valueKey, object value);
void Update(string key, string valueKey, object value);
object Get(string key, string valueKey);
void Remove(string key);
bool Exists(string key);
bool Exists(string key, string valueKey);
}
```
在介绍本地缓存的实现之前,让我们先来思考下本地缓存的实现思路——就是在本地缓存类中定义一个字典对象,添加缓存就是往该字典插入键值对而已,其中key就是缓存数据对应的键值,value就是真正的缓存数据,如果缓存在字典中存在的话,就直接根据键值查找出缓存数据进行返回。
然而网上书店的本地缓存是基于Enterprise Library Caching库来实现的,其实现思路和我之前介绍的思路也是一样的,只不过此时字典对象不需要我们在类中定义,此时直接用Enterprise Library Caching库中定义的就好。有了上面的分析,本地缓存的实现理解起来也就不那么难了,具体本地缓存的实现代码如下所示:
```
// 表示基于Microsoft Patterns & Practices - Enterprise Library Caching Application Block的缓存机制的实现
// 该类简单理解为对Enterprise Library Caching中的CacheManager封装
// 该缓存实现不支持分布式缓存,更多信息参考:
// http://stackoverflow.com/questions/7799664/enterpriselibrary-caching-in-load-balance
public class EntLibCacheProvider : ICacheProvider
{
// 获得CacheManager实例,该实例的注册通过cachingConfiguration进行注册进去的,具体看配置文件
private readonly ICacheManager _cacheManager = CacheFactory.GetCacheManager();
#region ICahceProvider
public void Add(string key, string valueKey, object value)
{
Dictionary dict = null;
if (_cacheManager.Contains(key))
{
dict = (Dictionary) _cacheManager[key];
dict[valueKey] = value;
}
else
{
dict = new Dictionary { { valueKey, value }};
}
_cacheManager.Add(key, dict);
}
public void Update(string key, string valueKey, object value)
{
Add(key, valueKey, value);
}
public object Get(string key, string valueKey)
{
if (!_cacheManager.Contains(key)) return null;
var dict = (Dictionary)_cacheManager[key];
if (dict != null && dict.ContainsKey(valueKey))
return dict[valueKey];
else
return null;
}
// 从缓存中移除对象
public void Remove(string key)
{
_cacheManager.Remove(key);
}
// 判断指定的键值的缓存是否存在
public bool Exists(string key)
{
return _cacheManager.Contains(key);
}
// 判断指定的键值和缓存键值的缓存是否存在
public bool Exists(string key, string valueKey)
{
return _cacheManager.Contains(key) &&
((Dictionary)_cacheManager[key]).ContainsKey(valueKey);
}
#endregion
}
```
到此,网上书店案例中本地缓存的实现就完成了。由于本地缓存不支持分布式部署,所有的缓存都存在于单独缓存服务器中,然而,针对一些大型网站来说,这样的实现并不适合,因为在大型网站中,需要通过多个缓存服务进行集群,需要使得缓存均匀分布在集群中的缓存服务器中。此时就需要引入分布式缓存的实现。下面让我们具体看看分布式缓存如何在该案例中实现。
## 3.2 分布式缓存的实现
分布式缓存可以通过具体的算法把缓存均匀地分布在集群中缓存服务器中,从而用户请求的不同数据可以路由到对应的缓存服务器中进行添加、更新或获得。分布式缓存的实现有很多种方式,可以利用Memcached和Redis开源库来实现。然而,微软的Windows Azure也提供了分布式缓存的实现,本案例中分布式缓存就是基于Windows Azure的。在对分布式缓存实现之前,需要先下载对应的dll,然后再在项目中进行引用。需要下载的dll已经包含在项目根目录下的libs文件夹下,具体需要下载的程序集截图如下所示:
![](http://images0.cnblogs.com/blog2015/383187/201506/152318026232985.png)
然后在基础设施层引入这些程序集,之前就可以去实现基于Windows Azure的分布式缓存了。具体的实现代码如下所示:
```
// 分布式缓存,该类是对微软分布式缓存服务的封装
// 在该案例中没用用到该缓存,但是提供在这里让大家明白微软的分布式缓存实现,并不是只有memcached和Redis
// Redis参考:http://www.cnblogs.com/ceecy/p/3279407.html 和 http://blog.csdn.net/suifeng3051/article/details/23739295
// 关于微软分布式缓存更多介绍参考:http://www.cnblogs.com/shanyou/archive/2010/06/29/AppFabricCaching.html
// 和http://www.cnblogs.com/mlj322/archive/2010/04/05/1704624.html
public class AppfabricCacheProvider : ICacheProvider
{
private readonly DataCacheFactory _factory = new DataCacheFactory();
private readonly DataCache _cache;
public AppfabricCacheProvider()
{
this._cache = _factory.GetDefaultCache();
}
#region ICacheProvider Members
public void Add(string key, string valueKey, object value)
{
// DataCache中不包含Contain方法,所有用Get方法来判断对应的key值是否在缓存中存在
var val = (Dictionary)_cache.Get(key);
if (val == null)
{
val = new Dictionary {{ valueKey, value}};
_cache.Add(key, val);
}
else
{
if (!val.ContainsKey(valueKey))
val.Add(valueKey, value);
else
val[valueKey] = value;
_cache.Put(key, val);
}
}
public void Update(string key, string valueKey, object value)
{
Add(key, valueKey, value);
}
public object Get(string key, string valueKey)
{
return Exists(key, valueKey) ? ((Dictionary)_cache.Get(key))[valueKey] : null;
}
public void Remove(string key)
{
_cache.Remove(key);
}
public bool Exists(string key)
{
return _cache.Get(key) != null;
}
public bool Exists(string key, string valueKey)
{
var val = _cache.Get(key);
if (val == null)
return false;
return ((Dictionary)val).ContainsKey(valueKey);
}
#endregion
}
```
通过上面的步骤,分布式缓存的实现就完成了。其实,分布式缓存和本地缓存不同之处就在于:分布式缓存支持对应的算法可以把缓存存放在不同的服务器上,而本地缓存只能存在于本地,而不能跨机器分布。所以对于大型网站,分布式缓存才是最好的选择,由于分布式缓存的实现和部署,无疑会增加开发和维护成本,所以对于一些小型系统(指定是单数据库服务器系统),可以考虑使用本地缓存。
在本案例中,由于本人没有Windows Azure环境,所以对于分布式缓存的实现也不能进行测试,所以本案例中使用的还是本地缓存。要使缓存生效,还需要对配置文件进行修改。具体配置文件修改为:
```
**
**
```
其实,通过上面的配置之后,缓存还是不能生效的,因为我们一般把缓存放在获得数据方法之前进行调用,在用户对获得数据方法调用之前,首先从缓存中进行查找,如果存在,则直接返回缓存中的数据给调用者就可以了,如果不存在再调用获得数据方法从数据库中读取,读取成功后添加到缓存中再返回给调用者。既然要在方法调用前来查找缓存,从中你是否想到了什么呢?不错,就是面向切面编程,即AOP。所以要让缓存生效,在该案例中还需要支持AOP。至于AOP的支持,我将会在下一专题进行介绍。
## 四、总结
到这里,本专题的内容就结束了,正如前面所说的,在下一专题,我将在网上书店案例中引入对AOP的支持。
本专题所有源码下载地址:[https://github.com/lizhi5753186/OnlineStore_Second/](https://github.com/lizhi5753186/OnlineStore_Second/)
';
.NET领域驱动设计实战系列 专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能
最后更新于:2022-04-02 00:14:08
# [.NET领域驱动设计实战系列]专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能
## 一、引言
在当前的电子商务平台中,用户下完订单之后,然后店家会在后台看到客户下的订单,然后店家可以对客户的订单进行发货操作。此时客户会在自己的订单状态看到店家已经发货。从上面的业务逻辑可以看出,当用户下完订单之后,店家或管理员可以对客户订单进行跟踪和操作。上一专题我们已经实现创建订单的功能,则接下来自然就是后台管理功能的实现了。所以在这一专题中将详细介绍如何在网上书店案例中实现后台管理功能。
## 二、后台管理中的权限管理的实现
后台管理中,首先需要实现的自然就是权限管理了,因为要进行商品管理等操作的话,则必须对不同的用户指定的不同角色,然后为不同角色指定不同的权限。这样才能确保普通用户不能进行一些后台操作。
然而角色和权限的赋予一般都是由系统管理员来操作。所以在最开始创建一个管理员用户,之后就可以以管理员的账号进行登录来进行后台操作的管理,包括添加角色,为用户分配角色、添加用户等操作。
这里就牵涉到一个权限管理的问题了。系统如何针对不同用户的全新进行管理呢?
其权限管理一个实现思路其实如下:
* 不同角色可以看到不同的链接,只有指定权限的用户才可以看到与其对应权限的操作。如只有管理员才可以添加用户和为用户赋予权限,而卖家只能对消费者订单的处理和对自己商店添加商品等操作。
从上面的描述可以发现,权限管理的实现主要包括两部分:
1. 为不同用户指定不同的链接显示。如管理员可以看到后台管理的所有链接:包括角色管理,商品管理,用户管理、订单管理,商品分类管理,而卖家只能看到订单管理,商品管理和商品类别管理等。其实现就是为这些链接的生成指定不同的权限,只有达到权限用户才进行生成该链接
2. 既然要为不同用户指定不同的权限,则首先要获得用户的权限,然后根据用户的权限来动态生成对应的链接。
有了上面的思路,下面就让我们一起为网上书店案例加入权限管理的功能:
首先,我在Layout.cshtml页面加入指定权限的链接,具体的代码如下所示:
```
```
上面红色加粗部分就是设置不同角色的不同权限。其中ActionLinkWithPermission是一个扩展方法,其具体实现就是获得登陆用户的角色,然后把用户的角色与当前的需要权限进行比较,如果相同,则通过HtmlHelper.GenerateLink方法来生成对应的链接。该方法的实现代码如下所示:
```
public static MvcHtmlString ActionLinkWithPermission(this HtmlHelper helper, string linkText, string action, string controller, PermissionKeys required)
{
if (helper == null ||
helper.ViewContext == null ||
helper.ViewContext.RequestContext == null ||
helper.ViewContext.RequestContext.HttpContext == null ||
helper.ViewContext.RequestContext.HttpContext.User == null ||
helper.ViewContext.RequestContext.HttpContext.User.Identity == null)
return MvcHtmlString.Empty;
using (var proxy = new UserServiceClient())
{
var role = proxy.GetRoleByUserName(helper.ViewContext.RequestContext.HttpContext.User.Identity.Name);
if (role == null)
return MvcHtmlString.Empty;
var keyName = role.Name;
var permissionKey = (PermissionKeys)Enum.Parse(typeof(PermissionKeys), keyName);
// 通过用户的角色和对应对应的权限进行与操作
// 与结果等于用户角色时,表示用户角色与所需要的权限一样,则创建对应权限的链接
return (permissionKey & required) == permissionKey ?
MvcHtmlString.Create(HtmlHelper.GenerateLink(helper.ViewContext.RequestContext, helper.RouteCollection, linkText, null, action, controller, null, null))
: MvcHtmlString.Empty;
}
}
```
通过上面的代码,我们就已经完成了权限管理的实现了。
## 三、后台管理中商品管理的实现
如果你是管理员的话,这样你就可以进入后台页面对商品、用户、订单等进行管理了。在上面我们已经完成了权限管理的实现。接下来,我们可以用一个管理员账号登陆之后,你可以看到管理员对应的权限。这里我直接在数据库中添加了一条管理员账号,其账号信息是admin,密码也是admin。下面我就这个账号后看到的界面如下图所示:
![](http://images0.cnblogs.com/blog2015/383187/201506/131522390199370.png)
从上图可以看出,后台管理包括销售订单管理、商品类别管理、商品信息管理等。这些都是一些类似的实现,都是一些增、删、改功能的实现。这里就是商品信息管理为例来介绍下。点击商品信息管理后,将可以看到所有商品列表,在该页面可以进商品进行添加、修改和删除等操作。其实现主要是通过应用服务来调用仓储来实现商品的信息的持久化罢了,下面就具体介绍下商品添加功能的实现。因为商品的添加需要首先把上传的图片先添加到服务器上的Images文件夹下,然后通过控制器来调用ProductService的CreateProducts方法来把商品保存到数据库中。
首先是图片上传功能的实现,其实现代码如下所示:
```
[HandleError]
public class AdminController : ControllerBase
{
#region Common Utility Actions
// 保存图片到服务器指定目录下
[NonAction]
private void SaveFile(HttpPostedFileBase postedFile, string filePath, string saveName)
{
string phyPath = Request.MapPath("~" + filePath);
if (!Directory.Exists(phyPath))
{
Directory.CreateDirectory(phyPath);
}
try
{
postedFile.SaveAs(phyPath + saveName);
}
catch (Exception e)
{
throw new ApplicationException(e.Message);
}
}
// 图片上传功能的实现
[HttpPost]
public ActionResult Upload(HttpPostedFileBase fileData, string folder)
{
var result = string.Empty;
if (fileData != null)
{
string ext = Path.GetExtension(fileData.FileName);
result = Guid.NewGuid()+ ext;
SaveFile(fileData, Url.Content("~/Images/Products/"), result);
}
return Content(result);
}
}
```
图片上传成功之后,接下来点击保存按钮则把商品进行持久化到数据库中。其实现逻辑主要是调用商品仓储的实现类来完成商品的添加。主要的实现代码如下所示:
```
[HandleError]
public class AdminController : ControllerBase
{
[HttpPost]
[Authorize]
public ActionResult AddProduct(ProductDto product)
{
using (var proxy = new ProductServiceClient())
{
if (string.IsNullOrEmpty(product.ImageUrl))
{
var fileName = Guid.NewGuid() + ".png";
System.IO.File.Copy(Server.MapPath("~/Images/Products/ProductImage.png"), Server.MapPath(string.Format("~/Images/Products/{0}", fileName)));
product.ImageUrl = fileName;
}
var addedProducts = proxy.CreateProducts(new List { product }.ToArray());
if (product.Category != null &&
product.Category.Id != Guid.Empty.ToString())
proxy.CategorizeProduct(new Guid(addedProducts[0].Id), new Guid(product.Category.Id));
return RedirectToSuccess("添加商品信息成功!", "Products", "Admin");
}
}
}
// 商品服务的实现
public class ProductServiceImp : ApplicationService, IProductService
{
public List CreateProducts(List productsDtos)
{
return PerformCreateObjects
';
|
- , ProductDto, Product>(productsDtos, _productRepository);
}
}
```
到此,我们已经完成了商品添加功能的实现,下面让我们看看商品添加的具体效果如何。添加商品页面:
![](http://images0.cnblogs.com/blog2015/383187/201506/131613046444421.png)
点击保存更改按钮后,则进行商品的添加,添加成功后界面效果:
![](http://images0.cnblogs.com/blog2015/383187/201506/131614524885112.png)
## 四、后台管理中发货操作和确认收货的实现
当消费者创建订单之后,然后卖家或管理员可以通过订单管理页面来对订单进行发货处理操作。以通知购买者该商品已发货了。在当前的电子商务网站中,除了更新订单的状态外,还会发邮件或短信通知购买者。为了保证这两个操作同时完成,此时需要将这两个放在同一个事务中进行提交。
这里为了使系统有更好地可扩展性,采用了基于消息队列和事件驱动的方式来完成发货操作。在看具体实现代码之前,我们先来分析下实现思路:
* 卖家或管理员在订单管理页面,点击发货按钮后,此时相当于订单的状态进行了更新,从已付款状态到已发货状态。这里当然你可以采用传统的方式来实现,即调用订单仓储来更新对应订单的状态。但是这样的实现方式,邮件发送操作可能会嵌套在应用服务层了。这样的设计显然不适合扩展。所以这里采用基于事件驱动和消息队列方式来改进这种方式。
1. 首先,当商家点击发货操作,此时会产生一个发货事件;
2. 接着由注册的领域事件处理程序进行对该领域事件处理,处理逻辑主要是更新订单的状态和更新时间;
3. 然后再将该事件发布到EventBus,EventBus中保存了一个队列来存放事件,发布操作的实现就是往该队列插入一个待处理的事件;
4. 最后在EventBus中的Commit方法中对队列中的事件进行出队列操作,通过事件聚合类来获得对应事件处理器来对出队列的事件进行处理。
* 事件聚合器通过Unity注入(应用)事件的处理器。在EventAggregator类中定义_eventHandlers来保存所有(应用)事件的处理器,在EventAggregator的构造函数中通过调用其Register方法把对应的事件处理器添加到_eventHandlers字典中。然后在EventBus中的Commit方法中通过找到EventAggregator中的Handle方法来触发事件处理器来处理对应事件,即发出邮件通知。这里事件聚合器起到映射的功能,映射应用事件到对应的事件处理器来处理。
通过上面的分析可以发现,发货操作和收货操作都涉及2类事件,一类是领域事件,另一类处于应用事件,领域事件的处理由领域事件处理器来处理,而应用事件的处理不能定义在领域层,所以我们这里新建了一个应用事件处理层,叫OnlineStore.Events.Handlers,已经新建了一个对EventBus支持的层,叫OnlineStore.Events。经过上面的分析,实现发货操作和收货操作是不是有点清晰了呢?如果不是的话也没关系,我们可以结合下面具体的实现代码再来理解下上面分析的思路。因为收货操作和发货操作的实现非常类似,这里只贴出发货操作实现的主要代码进行演示。
首先是AdminController中DispatchOrder操作的实现:
```
public ActionResult DispatchOrder(string id)
{
using (var proxy = new OrderServiceClient())
{
proxy.Dispatch(new Guid(id));
return RedirectToSuccess(string.Format("订单 {0} 已成功发货!", id.ToUpper()), "Orders", "Admin");
}
}
```
接下来便是OrderService中Dispatch方法的实现:
```
public void Dispatch(Guid orderId)
{
using (var transactionScope = new TransactionScope())
{
var order = _orderRepository.GetByKey(orderId);
order.Dispatch();
_orderRepository.Update(order);
RepositorytContext.Commit();
_eventBus.Commit();
transactionScope.Complete();
}
}
```
下面是Order实体类中Dispatch方法的实现:
```
///
.NET领域驱动设计实战系列 专题六:DDD实践案例:网上书店订单功能的实现
最后更新于:2022-04-02 00:14:06
# [.NET领域驱动设计实战系列]专题六:DDD实践案例:网上书店订单功能的实现
## 一、引言
上一专题已经为网上书店实现了购物车的功能了,在这一专题中,将继续对网上书店案例进行完善,本专题将对网上书店订单功能的实现进行介绍,现在废话不多说了,让我们来一起看看订单功能是如何实现的吧。
## 二、订单功能的实现思路
在网上购过物的朋友,对于订单功能的流程自然不陌生,这里我还是先来梳理下下订单的一个流程:
* 用户点击我的购物车,可以勾选对应的商品进行结算
* 在结算页面可以提交订单来创建一个订单
* 创建订单成功之后就是进行付款了。
一般购物网站下订单的流程分上面3步,由于在本案例中并没有对接第三方的支付平台,所以这里就没有上面第三步的过程了。即在网上书店案例中订单提交成功之后就是已付款状态。
从上面下订单流程我们就可以知道订单功能的实现思路:
* 用户点击购物车中购买商品按钮来进行下订单,此时触发控制器中的结算方法进行调用
* 结算方法通过调用OrderService服务中的结算方法
* 由于订单的创建涉及了3个实体操作,包括购物车实体,购物车项实体和订单实体。所以这里需要引入领域服务。因为创建订单这个操作涉及了多个实体,则这个业务逻辑放在每个实体中都不合适,因为并属于单独一个实体的逻辑。所以这里引入领域服务来实现这种涉及多个实体的操作。
* 则OrderService服务中的结算方法通过调用领域服务中的CreateOrder方法来完成订单创建的功能。
* 领域服务中可以调用对应的实体仓储来完成实体的持久化。这里需要注意的一点:因为领域服务涉及多个实体的持久化,则需要引入工作单元模式将这些实体的操作进行统一提交,要不都成功,要不都不成功。这也就是引入工作单元初衷。
上面的思路就是订单功能的实现思路。有了上面的思路之后,实现订单功能也一目了然了。下面让我们一起在网上书店案例中实现下看看。
## 三、网上书店订单功能的实现
这里我们按照上面的实现思路由下至上地去实现订单功能。
1. 首先我们需要订单仓储来完成订单实体的持久化。具体订单仓储接口和实现如下代码所示:
```
// 订单仓储接口
public interface IOrderRepository : IRepository
{
}
// 订单仓储的实现类
public class OrderRepository : EntityFrameworkRepository, IOrderRepository
{
public OrderRepository(IRepositoryContext context) : base(context)
{
}
}
```
2\. 领域服务的实现。具体的领域服务接口和实现代码如下所示:
```
// 领域服务接口
public interface IDomainService
{
Order CreateOrder(User user, ShoppingCart shoppingCart);
}
// 领域服务类型
// 牵涉到多个实体的操作可以把这些操作封装到领域服务里
public class DomainService : IDomainService
{
private readonly IRepositoryContext _repositoryContext;
private readonly IShoppingCartItemRepository _shoppingCartItemRepository;
private readonly IOrderRepository _orderRepository;
///
/// 创建订单,涉及到的操作有2个:1\. 把购物车中的项中购物车移除; 2.创建一个订单。
/// 这两个操作必须同时完成或失败。
///
///
///
///
public Order CreateOrder(User user, ShoppingCart shoppingCart)
{
var order = new Order();
var shoppingCartItems =
_shoppingCartItemRepository.GetAll(
new ExpressionSpecification(s => s.ShoopingCart.Id == shoppingCart.Id));
if (shoppingCartItems == null || !shoppingCartItems.Any())
throw new InvalidOperationException("购物篮中没有任何物品");
order.OrderItems = new List();
foreach (var shoppingCartItem in shoppingCartItems)
{
var orderItem = shoppingCartItem.ConvertToOrderItem();
orderItem.Order = order;
order.OrderItems.Add(orderItem);
_shoppingCartItemRepository.Remove(shoppingCartItem);
}
order.User = user;
order.Status = OrderStatus.Paid;
_orderRepository.Add(order);
_repositoryContext.Commit();
return order;
}
}
```
3\. 订单服务的实现。具体订单服务实现代码如下所示:
```
public class OrderServiceImp : ApplicationService, IOrderService
{
#region Private Fileds
private readonly IShoppingCartRepository _shoppingCartRepository;
private readonly IShoppingCartItemRepository _shoppingCartItemRepository;
private readonly IUserRepository _userRepository;
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
private readonly IDomainService _domainService;
private readonly IEventBus _eventBus;
#endregion
#region Ctor
public OrderServiceImp(IRepositoryContext context,
IUserRepository userRepository,
IShoppingCartRepository shoppingCartRepository,
IProductRepository productRepository,
IShoppingCartItemRepository shoppingCartItemRepository,
IDomainService domainService,
IOrderRepository orderRepository,
IEventBus eventBus) : base(context)
{
_userRepository = userRepository;
_shoppingCartRepository = shoppingCartRepository;
_productRepository = productRepository;
_shoppingCartItemRepository = shoppingCartItemRepository;
_domainService = domainService;
_orderRepository = orderRepository;
_eventBus = eventBus;
}
#endregion
public OrderDto Checkout(Guid customerId)
{
var user = _userRepository.GetByKey(customerId);
var shoppingCart = _shoppingCartRepository.GetByExpression(s => s.User.Id == user.Id);
var order = _domainService.CreateOrder(user, shoppingCart);
return Mapper.Map(order);
}
public OrderDto GetOrder(Guid orderId)
{
var order = _orderRepository.GetBySpecification(new ExpressionSpecification(o=>o.Id.Equals(orderId)), elp=>elp.OrderItems);
return Mapper.Map(order);
}
// 获得指定用户的所有订单
public IList GetOrdersForUser(Guid userId)
{
var user = _userRepository.GetByKey(userId);
var orders = _orderRepository.GetAll(new ExpressionSpecification(o => o.User.Id == userId), sp => sp.CreatedDate, SortOrder.Descending, elp=>elp.OrderItems);
var orderDtos = new List();
orders
.ToList()
.ForEach(o=>orderDtos.Add(Mapper.Map(o)));
return orderDtos;
}
```
4\. HomeController控制器中Checkout操作的实现。具体实现代码如下所示:
```
public class HomeController : ControllerBase
{
///
/// 结算操作
///
///
[Authorize]
public ActionResult Checkout()
{
using (var proxy = new OrderServiceClient())
{
var model = proxy.Checkout(this.UserId);
return View(model);
}
}
[Authorize]
public ActionResult Orders()
{
using (var proxy = new OrderServiceClient())
{
var model = proxy.GetOrdersForUser(this.UserId);
return View(model);
}
}
[Authorize]
public ActionResult Order(string id)
{
using (var proxy = new OrderServiceClient())
{
var model = proxy.GetOrder(new Guid(id));
return View(model);
}
}
}
```
这样我们就在网上书店中实现了订单功能了。具体的视图界面也就是上一专题中实现的购物车页面。下面具体看看订单的具体实现效果:
结算页面:
![](http://images0.cnblogs.com/blog2015/383187/201506/082252139104115.png)
点击确认购买按钮,在弹出框中点击确认来完成订单的创建:
![](http://images0.cnblogs.com/blog2015/383187/201506/082254206443667.png)
通过我的订单来查看所有订单页面:
![](http://images0.cnblogs.com/blog2015/383187/201506/082259235041793.png)
## 四、总结
到此,网上书店案例的订单功能的实现就完成了,在接下来的专题将继续对该案例进行完善,在下一专题中将为该案例引入后台管理操作。商家或管理员可以进入后台管理来对用户订单进行确认发货,以及添加商品,分类等操作。具体实现请见下一专题。
本专题中所有实现源码下载:https://github.com/lizhi5753186/OnlineStore_Second/
';
.NET领域驱动设计实战系列 专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
最后更新于:2022-04-02 00:14:04
# [.NET领域驱动设计实战系列]专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
## 一、前言
在前面2篇博文中,我分别介绍了规约模式和工作单元模式,有了前面2篇博文的铺垫之后,下面就具体看看如何把这两种模式引入到之前的网上书店案例里。
## 二、规约模式的引入
在[第三专题](http://www.cnblogs.com/zhili/p/SpecificationPattern.html)我们已经详细介绍了什么是规约模式,没看过的朋友首先去了解下。下面让我们一起看看如何在网上书店案例中引入规约模式。在网上书店案例中规约模式的实现兼容了2种模式的实现,兼容了传统和轻量的实现,包括传统模式的实现,主要是为了实现一些共有规约的重用,不然的话可能就要重复写这些表达式。下面让我们具体看看在该项目中的实现。
首先是ISpecification接口的定义以及其抽象类的实现
```
public interface ISpecification
{
bool IsSatisfiedBy(T candidate);
Expression> Expression { get; }
}
public abstract class Specification : ISpecification
{
public static Specification Eval(Expression> expression)
{
return new ExpressionSpecification(expression);
}
#region ISpecification Members
public bool IsSatisfiedBy(T candidate)
{
return this.Expression.Compile()(candidate);
}
public abstract Expression> Expression { get; }
#endregion
}
```
上面的实现稍微对传统规约模式进行了一点修改,添加 Expression属性来获得规约的表达式树。另外,在该案例中还定义了一个包装表达式树的规约类和没有任何条件的规约类AnySpecification。其具体实现如下:
```
public sealed class ExpressionSpecification : Specification
{
private readonly Expression> _expression;
public ExpressionSpecification(Expression> expression)
{
this._expression = expression;
}
public override Expression> Expression
{
get { return _expression; }
}
}
public sealed class AnySpecification : Specification
{
public override Expression> Expression
{
get { return o => true; }
}
}
```
接下来就是轻量规约模式的实现了,该实现涉及2个类,一个是包含扩展方法的类和一个实现统一表达式树参数的类。具体实现代码如下所示:
```
public static class SpecExprExtensions
{
public static Expression> Not(this Expression> one)
{
var candidateExpr = one.Parameters[0];
var body = Expression.Not(one.Body);
return Expression.Lambda>(body, candidateExpr);
}
public static Expression> And(this Expression> one,
Expression> another)
{
// 首先定义好一个ParameterExpression
var candidateExpr = Expression.Parameter(typeof(T), "candidate");
var parameterReplacer = new ParameterReplacer(candidateExpr);
// 将表达式树的参数统一替换成我们定义好的candidateExpr
var left = parameterReplacer.Replace(one.Body);
var right = parameterReplacer.Replace(another.Body);
var body = Expression.And(left, right);
return Expression.Lambda>(body, candidateExpr);
}
public static Expression> Or(
this Expression> one, Expression> another)
{
var candidateExpr = Expression.Parameter(typeof(T), "candidate");
var parameterReplacer = new ParameterReplacer(candidateExpr);
var left = parameterReplacer.Replace(one.Body);
var right = parameterReplacer.Replace(another.Body);
var body = Expression.Or(left, right);
return Expression.Lambda>(body, candidateExpr);
}
}
public class ParameterReplacer : ExpressionVisitor
{
public ParameterReplacer(ParameterExpression paramExpr)
{
this.ParameterExpression = paramExpr;
}
public ParameterExpression ParameterExpression { get; private set; }
public Expression Replace(Expression expr)
{
return this.Visit(expr);
}
protected override Expression VisitParameter(ParameterExpression p)
{
return this.ParameterExpression;
}
}
```
这样,规约模式在案例中的实现就完成了,下面具体介绍下工作单元模式是如何在该项目中实现的。
## 三、工作单元模式的引入
工作单元模式,主要是为了保证数据的一致性,一些涉及到多个实体的操作我们希望它们一起被提交,从而保证数据的正确性和一致性。例如,在该项目,用户成功注册的同时需要为用户创建一个购物车对象,这里就涉及到2个实体,一个是用户实体,一个是购物车实体,所以此时必须保证这个操作必须作为一个操作被提交,这样就可以保证要么一起提交成功,要么都失败,不存在其中一个被提交成功的情况,否则就会出现数据不正确的情况,上一专题的转账业务也是这个道理,只是转账业务涉及的是2个相同的实体,都是账户实体。
从上面描述可以发现,**要保证数据的一致性,必须要有一个类统一管理提交操作,而不能由其仓储实现来提交数据改变**。根据上一专题我们可以知道,首先需要定义一个工作单元接口IUnitOfWork,工作单元接口的定义通常放在基础设施层,其定义代码如下所示:
在该项目中,对工作单元接口的方法进行了一个分离,把其方法分别定义在2个接口中,工作单元接口中仅仅定义了一个Commit方法,RegisterNew, RegisterModified和RegisterDelete方法定义在IRepositoryContext接口中。当然我觉得也可以把这4个操作都定义在IUnitOfWork接口中。这里只是遵循dax.net案例中设计来实现的。然后EntityFrameworkRepositoryContext来实现这4个操作。工作单元模式在项目中的实现代码如下所示:
```
// 仓储上下文接口
// 这里把传统的IUnitOfWork接口中方法分别在2个接口定义:一个是IUnitOfWork,另一个就是该接口
public interface IRepositoryContext : IUnitOfWork
{
// 用来标识仓储上下文
Guid Id { get; }
void RegisterNew(TAggregateRoot entity)
where TAggregateRoot : class, IAggregateRoot;
void RegisterModified(TAggregateRoot entity)
where TAggregateRoot : class, IAggregateRoot;
void RegisterDeleted(TAggregateRoot entity)
where TAggregateRoot : class, IAggregateRoot;
}
public interface IEntityFrameworkRepositoryContext : IRepositoryContext
{
#region Properties
OnlineStoreDbContext DbContex { get; }
#endregion
}
// IEntityFrameworkRepositoryContext接口的实现
public class EntityFrameworkRepositoryContext : IEntityFrameworkRepositoryContext
{
// ThreadLocal代表线程本地存储,主要相当于一个静态变量
// 但静态变量在多线程访问时需要显式使用线程同步技术。
// 使用ThreadLocal变量,每个线程都会一个拷贝,从而避免了线程同步带来的性能开销
private readonly ThreadLocal _localCtx = new ThreadLocal(() => new OnlineStoreDbContext());
public OnlineStoreDbContext DbContex
{
get { return _localCtx.Value; }
}
private readonly Guid _id = Guid.NewGuid();
#region IRepositoryContext Members
public Guid Id
{
get { return _id; }
}
public void RegisterNew(TAggregateRoot entity) where TAggregateRoot : class, Domain.IAggregateRoot
{
_localCtx.Value.Set().Add(entity);
}
public void RegisterModified(TAggregateRoot entity) where TAggregateRoot : class, Domain.IAggregateRoot
{
_localCtx.Value.Entry(entity).State = EntityState.Modified;
}
public void RegisterDeleted(TAggregateRoot entity) where TAggregateRoot : class, Domain.IAggregateRoot
{
_localCtx.Value.Set().Remove(entity);
}
#endregion
#region IUnitOfWork Members
public void Commit()
{
var validationError = _localCtx.Value.GetValidationErrors();
_localCtx.Value.SaveChanges();
}
#endregion
}
```
到此,工作单元模式的引入也就完成了,接下面,让我们继续完成网上书店案例。
## 四、购物车的实现
在前一个版本中,只是实现了商品的展示和详细信息等功能。在网上商店中,都有购物车这个功能,作为一个完整的案例,该案例也不能少了这个功能。在实现购物车之前,我们首先理清下业务逻辑:访问者看到商品,然后点击商品下的加入购物车按钮,把商品加入购物车。
在上面的业务逻辑中,涉及了下面几个更细的业务逻辑:
* 如果用户没注册时,访客点击加入购物车按钮应跳转到注册界面,这样就涉及到用户注册功能的实现
* 用户注册成功后需要同时为用户创建一个购物车实例与该用户进行绑定,之后用户就可以把商品加入自己的购物车
* 加入购物车之后,用户可以查看购物车中的商品,同时也应该可以进行更新和移除操作。
通过上面的描述,大家应该自然明白了我们接下来需要哪些功能了吧,下面我们来理理:
1. 用户注册功能,涉及用户注册页面。自然就涉及用户注册服务和用户仓储的实现
2. 注册成功同时创建购物车实例。自然涉及创建购物车服务方法和购物车仓储的实现
3. 加入购物车成功后,可以查看购物车中的商品、更新和移除操作,自然涉及到购物车页面的实现。这里自然涉及到获得购物车和更新商品数量和删除购物项的服务方法。
理清了思路之后,接下来就应该去实现功能了,首先应该实现自然就是用户注册模块。实现功能模块都从底向上来实现,首先应该先定义用户聚合根,接着实现用户仓储和用户服务,最后实现控制器和视图。下面是用户注册涉及的主要类的实现:
```
// 用户聚合根
public class User : AggregateRoot
{
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public bool IsDisabled { get; set; }
public DateTime RegisteredDate { get; set; }
public DateTime? LastLogonDate { get; set; }
public string Contact { get; set; }
//用户的联系地址
public Address ContactAddress { get; set; }
//用户的发货地址
public Address DeliveryAddress { get; set; }
public IEnumerable Orders
{
get
{
IEnumerable result = null;
//DomainEvent.Publish(new GetUserOrdersEvent(this),
// (e, ret, exc) =>
// {
// result = e.Orders;
// });
return result;
}
}
public override string ToString()
{
return this.UserName;
}
#region Public Methods
public void Disable()
{
this.IsDisabled = true;
}
public void Enable()
{
this.IsDisabled = false;
}
// 为当前用户创建购物篮。
public ShoppingCart CreateShoppingCart()
{
var shoppingCart = new ShoppingCart { User = this };
return shoppingCart;
}
#endregion
}
public interface IUserRepository : IRepository
{
bool CheckPassword(string userName, string password);
}
public class UserRepository : EntityFrameworkRepository, IUserRepository
{
public UserRepository(IRepositoryContext context)
: base(context)
{
}
public bool CheckPassword(string userName, string password)
{
Expression> userNameExpression = u => u.UserName == userName;
Expression> passwordExpression = u => u.Password == password;
return Exists(new ExpressionSpecification(userNameExpression.And(passwordExpression)));
}
}
// 用户服务契约
[ServiceContract(Namespace = "")]
public interface IUserService
{
#region Methods
[OperationContract]
[FaultContract(typeof (FaultData))]
IList CreateUsers(List userDtos);
[OperationContract]
[FaultContract(typeof(FaultData))]
bool ValidateUser(string userName, string password);
[OperationContract]
[FaultContract(typeof(FaultData))]
bool DisableUser(UserDto userDto);
[OperationContract]
[FaultContract(typeof(FaultData))]
bool EnableUser(UserDto userDto);
[OperationContract]
[FaultContract(typeof(FaultData))]
void DeleteUsers(UserDto userDto);
[OperationContract]
[FaultContract(typeof(FaultData))]
IList UpdateUsers(List userDataObjects);
[OperationContract]
[FaultContract(typeof (FaultData))]
UserDto GetUserByKey(Guid id);
[OperationContract]
[FaultContract(typeof(FaultData))]
UserDto GetUserByEmail(string email);
[OperationContract]
[FaultContract(typeof(FaultData))]
UserDto GetUserByName(string userName);
#endregion
}
public class **UserServiceImp** :ApplicationService, IUserService
{
private readonly IUserRepository _userRepository;
private readonly IShoppingCartRepository _shoppingCartRepository;
public UserServiceImp(IRepositoryContext repositoryContext,
IUserRepository userRepository,
IShoppingCartRepository shoppingCartRepository)
: base(repositoryContext)
{
_userRepository = userRepository;
_shoppingCartRepository = shoppingCartRepository;
}
public IList CreateUsers(List userDtos)
{
if (userDtos == null)
throw new ArgumentNullException("userDtos");
return PerformCreateObjects
';
- , UserDto, User>(userDtos,
_userRepository,
dto =>
{
if (dto.RegisterDate == null)
dto.RegisterDate = DateTime.Now;
},
ar =>
{
var shoppingCart = ar.CreateShoppingCart();
_shoppingCartRepository.Add(shoppingCart);
});
}
public bool ValidateUser(string userName, string password)
{
if (string.IsNullOrEmpty(userName))
throw new ArgumentNullException("userName");
if (string.IsNullOrEmpty(password))
throw new ArgumentNullException("password");
return _userRepository.CheckPassword(userName, password);
}
public bool DisableUser(UserDto userDto)
{
if(userDto == null)
throw new ArgumentNullException("userDto");
User user;
if (!IsEmptyGuidString(userDto.Id))
user = _userRepository.GetByKey(new Guid(userDto.Id));
else if (!string.IsNullOrEmpty(userDto.UserName))
user = _userRepository.GetByExpression(u=>u.UserName == userDto.UserName);
else if (!string.IsNullOrEmpty(userDto.Email))
user = _userRepository.GetByExpression(u => u.Email == userDto.Email);
else
throw new ArgumentNullException("userDto", "Either ID, UserName or Email should be specified.");
user.Disable();
_userRepository.Update(user);
RepositorytContext.Commit();
return user.IsDisabled;
}
public bool EnableUser(UserDto userDto)
{
if (userDto == null)
throw new ArgumentNullException("userDto");
User user;
if (!IsEmptyGuidString(userDto.Id))
user = _userRepository.GetByKey(new Guid(userDto.Id));
else if (!string.IsNullOrEmpty(userDto.UserName))
user = _userRepository.GetByExpression(u => u.UserName == userDto.UserName);
else if (!string.IsNullOrEmpty(userDto.Email))
user = _userRepository.GetByExpression(u => u.Email == userDto.Email);
else
throw new ArgumentNullException("userDto", "Either ID, UserName or Email should be specified.");
user.Enable();
_userRepository.Update(user);
RepositorytContext.Commit();
return user.IsDisabled;
}
public IList
.NET领域驱动设计实战系列 专题四:前期准备之工作单元模式(Unit Of Work)
最后更新于:2022-04-02 00:14:01
# [.NET领域驱动设计实战系列]专题四:前期准备之工作单元模式(Unit Of Work)
## 一、前言
在前一专题中介绍了规约模式的实现,然后在仓储实现中,经常会涉及工作单元模式的实现。然而,在我的网上书店案例中也将引入工作单元模式,所以本专题将详细介绍下该模式,为后面案例的实现做一个铺垫。
## 二、什么是工作单元模式(Unit Of Work)
工作单元模式:用来维护一个已经被业务事务修改(包括添加、修改或更新)的业务对象列表。工作单元模式复制协调这些修改的持久化工作以及所有标记的并发问题。采用工作单元模式带来的好处是能够保证数据的完整性。如果在持久化一系列业务对象的过程中出现问题,则将所有的修改回滚,以保证数据始终处于有效状态。
简单来说,工作单元模式就是把业务对象的持久化由工作单元实现类进行统一管理。而不想之前那样,分布在每个具体的仓储类中,这样就可以达到一系列业务对象的统一管理,不至于在每个业务对象中出现统一的提交和回滚业务逻辑,实现代码最大化重用。
## 三、工作单元模式的实现
从工作单元模式的定义可以看出,工作单元需要保存被业务事务修改的业务对象列表,则必须定义3个集合,分别是添加、修改和更新集合,然而如果使用EF的话,则不需要了,因为DbContext.DbSet<T>就可以代替这三个集合。这里为了演示,我们并没有引入EF,所以我们实现中定义了3个集合来保存被修改的业务对象。既然要在工作单元类中进行统一管理,则我们就需要在工作单元类中定义一个Commit方法,该方法的实现就是去遍历这三个集合的对象,对它们进行统一提交,如果其中一个失败,则进行数据回滚。根据面向接口编程原则,我们需要定义一个工作单元接口,即IUnitOfWork接口。经过上面的分析,再结合下面具体的实现来理解,工作单元模式的实现也就更加清晰了,下面让我们一起去实现下工作单元模式。
**第一步:我们需要定义我们的领域层。**
这里以银行账号之间转账业务作为背景,自然涉及到银行账号业务对象。并且领域层同时包括仓储接口的定义和领域服务。
领域服务指的是:如果有些方法涉及多个实体或聚合的交互,此时就应该把这段逻辑放到领域服务中,领域服务只有方法没有属性,也就是没有状态的。在银行转账业务中,账户之间的转账操作就适合作为领域服务来提供,因为转账操作涉及多个聚合间的交互,需要从一个账户扣钱和另一个账户加钱。所以在领域层还需要定义账户转账服务。经过这样的分析,则领域层的实现如下所示:
```
// 账号仓储接口
public interface IAccountRepository
{
void Save(Account account);
void Add(Account account);
void Remove(Account account);
}
// 账号实体类
public class Account : IAggregateRoot
{
public decimal Balance { get; set; }
public System.Guid Id { get; set; }
public Account()
{
Id = Guid.NewGuid();
}
}
// 账号转账领域服务类
public class AccountService
{
private readonly IAccountRepository _productRepository;
private readonly IUnitOfWork _unitOfWork;
public AccountService(IAccountRepository productRepository, IUnitOfWork unitOfWork)
{
_productRepository = productRepository;
_unitOfWork = unitOfWork;
}
public void Transfer(Account from, Account to, decimal amount)
{
if (from.Balance >= amount)
{
from.Balance -= amount;
to.Balance += amount;
_productRepository.Save(from);
_productRepository.Save(to);
_unitOfWork.Commit();
}
}
}
```
** 第二步:构建基础设施层**
我们一般把工作单元模式的实现放在基础设施层,因为工作单元模式属于一种技术支持。根据上面工作单元模式的分析,我们首先定义IUnitOfWork接口,接着定义它的实现。因为这里没有引入EF,所以具体的实体的持久化还是调用具体的仓储来实现持久化的,所以还需要定义一个IUnitOfWorkRepository接口。则基础设施层的实现如下所示:
```
// 工作单元接口
public interface IUnitOfWork
{
void RegisterAmended(IAggregateRoot entity, IUnitOfWorkRepository unitofWorkRepository);
void RegisterNew(IAggregateRoot entity, IUnitOfWorkRepository unitofWorkRepository);
void RegisterRemoved(IAggregateRoot entity, IUnitOfWorkRepository unitofWorkRepository);
void Commit();
}
// 工作单元实现
public class UnitOfWork : IUnitOfWork
{
// 引入了EF就不需要额外定义三个列表了,因为EF框架中包含的DbContext.DbSet可以记录这3个列表
// 然而在ByteartRetail案例中,也定义这3个列表,但其没有被真真使用到
private readonly Dictionary _addedEntities;
private readonly Dictionary _changedEntities;
private readonly Dictionary _deletedEntities;
public UnitOfWork()
{
_addedEntities = new Dictionary();
_changedEntities = new Dictionary();
_deletedEntities = new Dictionary();
}
// 将业务对象实体添加到内部列表中,真正完成实体持久化操作的还是由具体的仓储类去完成
public void RegisterAmended(IAggregateRoot entity, IUnitOfWorkRepository unitofWorkRepository)
{
if (!_changedEntities.ContainsKey(entity))
{
_changedEntities.Add(entity, unitofWorkRepository);
}
}
public void RegisterNew(IAggregateRoot entity, IUnitOfWorkRepository unitofWorkRepository)
{
if (!_addedEntities.ContainsKey(entity))
{
_addedEntities.Add(entity, unitofWorkRepository);
};
}
public void RegisterRemoved(IAggregateRoot entity, IUnitOfWorkRepository unitofWorkRepository)
{
if (!_deletedEntities.ContainsKey(entity))
{
_deletedEntities.Add(entity, unitofWorkRepository);
}
}
protected void ClearRegisterations()
{
_addedEntities.Clear();
_changedEntities.Clear();
_deletedEntities.Clear();
}
// 对内部列表进行统一提交
// 引入EF后,提交的实现有点不同,它具体的持久化只需要调用DbContext.SaveChanges方法来完成
// 则具体的仓储接口不需要实现IUnitOfWorkRepository接口,则自然不存在IUnitOfWorkRepository接口的定义
public void Commit()
{
// 事务范围
using (var scope = new TransactionScope())
{
// 分别调用具体的仓储对象的持久化逻辑来对业务对象进行持久化
foreach (var entity in this._addedEntities.Keys)
{
this._addedEntities[entity].PersistCreationOf(entity);
}
foreach (var entity in this._changedEntities.Keys)
{
this._changedEntities[entity].PersistUpdateOf(entity);
}
foreach (var entity in this._deletedEntities.Keys)
{
this._deletedEntities[entity].PersistDeletionOf(entity);
}
scope.Complete();
}
// 清楚内存中对象
ClearRegisterations();
}
}
public interface IUnitOfWorkRepository
{
Hashtable AccountList { get; }
void PersistCreationOf(IAggregateRoot entity);
void PersistUpdateOf(IAggregateRoot entity);
void PersistDeletionOf(IAggregateRoot entity);
}
public interface IAggregateRoot
{
Guid Id { get; }
}
```
**第三步:实现仓储层。**
仓储的实现在之前也说过,它其实可以放在基础设施层里,但一般总将其放在一个单独层进行实现。所以这里也就放在一个单独层进行实现。这里仓储实现只有一个类,即对IAccountRepository接口的实现。具体的实现代码如下所示:
```
public class AccountRepository : IAccountRepository, IUnitOfWorkRepository
{
private readonly IUnitOfWork _unitOfWork;
public AccountRepository(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
AccountList = new Hashtable();
}
public Hashtable AccountList { get; set; }
#region IAccountRepository Members
public void Save(Account account)
{
_unitOfWork.RegisterAmended(account, this);
}
public void Add(Account account)
{
_unitOfWork.RegisterNew(account, this);
}
public void Remove(Account account)
{
_unitOfWork.RegisterRemoved(account, this);
}
#endregion
#region IUnitOfWorkRepository Members
public void PersistUpdateOf(IAggregateRoot entity)
{
// ADO.net code to update the entity...
// 这里为了演示,只它持久化到内存中
if (AccountList.ContainsKey(entity.Id))
{
AccountList[entity.Id] = entity;
}
}
public void PersistCreationOf(IAggregateRoot entity)
{
// ADO.net code to Add the entity...
// 这里为了演示,只它持久化到内存中
AccountList.Add(entity.Id, entity);
}
public void PersistDeletionOf(IAggregateRoot entity)
{
// ADO.net code to delete the entity...
// 这里为了演示,只它持久化到内存中
if (AccountList.ContainsKey(entity.Id))
{
AccountList.Remove(entity.Id);
}
}
#endregion
}
```
这样,就完成了工作单元模式模式的实现和应用了,工作单元模式的实现存在于基础设施层,其他层的构建主要为了演示,下面通过一个测试项目来进行测试下工作单元的使用。具体项目的引入将会在下一个专题中介绍,下一个专题将引入工作单元模式和规约模式的实现。具体的测试项目的代码如下所示:
```
[TestClass]
public class AccountRepositoryTests
{
[TestMethod]
public void AccountRepository_Delegates_Changes_To_The_Unit_Of_Work_Instance()
{
var accountToBeAmended = new Account();
var accountToBeRemoved = new Account();
var accountToBeAdded = new Account();
// 需要引入Moq Mock框架
var unitOfWorkMockery = new Mock();
var accountRepository = new AccountRepository(unitOfWorkMockery.Object);
unitOfWorkMockery.Setup(uow => uow.RegisterAmended(accountToBeAmended, accountRepository));
unitOfWorkMockery.Setup(uow => uow.RegisterNew(accountToBeAdded, accountRepository));
unitOfWorkMockery.Setup(uow => uow.RegisterRemoved(accountToBeRemoved, accountRepository));
accountRepository.Add(accountToBeAdded);
accountRepository.Save(accountToBeAmended);
accountRepository.Remove(accountToBeRemoved);
unitOfWorkMockery.VerifyAll();
}
}
```
## 四、总结
到这里,本专题的内容就介绍完了,上一专题和这一专题都是一个前期准备的专题,主要是为网上书店案例引入这2个模式的实现做一个铺垫,为了让大家对知识点分开吸收,然后再通过后面一专题的应用来加深理解这2个模式的应用。
本专题的所有源码下载:[UnitOfWorkDemo.zip](http://files.cnblogs.com/files/zhili/UnitOfWorkDemo.zip)
';
.NET领域驱动设计实战系列 专题三:前期准备之规约模式(Specification Pattern)
最后更新于:2022-04-02 00:13:59
# [.NET领域驱动设计实战系列]专题三:前期准备之规约模式(Specification Pattern)
## 一、前言
在专题二中已经应用DDD和SOA的思想简单构建了一个网上书店的网站,接下来的专题中将会对该网站补充更多的DDD的内容。本专题作为一个准备专题,因为在后面一个专题中将会网上书店中的仓储实现引入规约模式。本专题将详细介绍了规约模式。
## 二、什么是规约模式
讲到规约模式,自然想到的是什么是规约模式呢?从名字上看,**规约模式就是一个约束条件**,我们在使用仓储进行查询的时候,这时候就会牵涉到很多查询条件,例如名字包含C#的书名等条件。这样就自然需要引入[规约模式](http://en.wikipedia.org/wiki/Specification_pattern)了。规约模式的作用可以自由组装业务逻辑元素。Specification类有一个IsSatisifiedBy函数,用于校验某个对象是否满足该Specification所表达的条件。多个Specification对象可以组装起来,生成新的Specification对象,这样可以通过组装的方式来定制新的条件。简单地说,规约模式就是对查询条件表达式用类的形式进行封装。那这样的话,规约模式引入有什么作用呢?
## 三、为什么需要引入规约模式模式
上面只是简单介绍了规约模式的作用——可以自由组装业务逻辑元素。这样文字表述未免枯燥了点,下面通过一个具体例子来说明下。
对于在仓储中,我们经常会定义下面的接口
接下来就是实现这个接口,并在类中分别实现接口中的方法。这样设计的好处就是一目了然,可以方便地看到Product仓储到底提供了哪些功能。
对于这种设计,对于简单系统并且今后扩展的可能性不大,那么这样的设计非常合适,因为其简洁高效。但如果你正在设计一个中大型系统,那么,针对上面的设计,你就需要考虑下面的问题了:
1. 今后如果需要添加新的查询逻辑,结果一大堆相关代码都需要修改,上面的设计能便于扩展吗?
2. 由于业务的扩展,上面的设计会导致接口变得越来越大,团队成员可能会对这个接口进行修改,添加新的接口方法。
规约模式就是DDD引入解决上面问题的一种模式。下面让我们来看看规约模式的定义与实现。
## 四、规约模式的传统实现
首先来看下规约模式的类结构图:
![specification-pattern-uml](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4b88422.png)
上图是摘自维基百科里面的,通过设计图我们很容易实现规约模式,这样之所以称为的传统实现,因为后面会对该实现应用C#的特性来对该实现进行简化,使其更加简单轻量。首先我们需要定义一个ISpecification接口,在接口中定义四个方法:And、Not、Or和IsSatifiedBy方法,具体接口的定义如下所示:
```
// 规约接口的定义
public interface ISpecification
{
bool IsSatisfiedBy(T candidate);
ISpecification And(ISpecification specification);
ISpecification Or(ISpecification specification);
ISpecification Not(ISpecification specification);
}
```
实现了ISpeification的对象意味着是一个Specification,即一种筛选条件,我们可以与其他Specification对象通过And、Or和Not操作来生成新的逻辑,即组合成新的筛选条件,为了方便“组合逻辑”的实现,这里还需要定义一个抽象的CompositeSpecification类:
```
// 因为And,OR和Not方法在所有的Specification都需要实现,只有IsSatisfiedBy方法才依赖业务规则
// 所以为了复用,定义一个抽象类来实现And,Or和And操作,并且留IsSatisfiedBy方法给子类去实现,所以定义其为abstract
public abstract class CompositeSpecification: ISpecification
{
public abstract bool IsSatisfiedBy(T candidate);
public ISpecification And(ISpecification specification)
{
return new AndSpecification(this, specification);
}
public ISpecification Or(ISpecification specification)
{
return new OrSpecification(this, specification);
}
public ISpecification Not(ISpecification specification)
{
return new NotSpecification(specification);
}
}
```
CompositeSpecification提供了构建符合Specification的基础逻辑,它提供了And、Or和Not方法的实现,让其他Specification类只需要专注于IsSatisfiedBy方法的实现即可(这里有点模板方法模式的影子)。下面是And、Or和Not规约的具体实现:
```
// AndSpecification,OrSpecification and NotSpecification主要为了组合
**public class AndSpecification : CompositeSpecification**
{
private readonly ISpecification _lefSpecification;
private readonly ISpecification _rightSpecification;
public AndSpecification(ISpecification left, ISpecification right)
{
this._lefSpecification = left;
this._rightSpecification = right;
}
public override bool IsSatisfiedBy(T candidate)
{
return this._lefSpecification.IsSatisfiedBy(candidate)
&& this._rightSpecification.IsSatisfiedBy(candidate);
}
}
**public class OrSpecification : CompositeSpecification**
{
private readonly ISpecification _leftSpecification;
private readonly ISpecification _rightSpecification;
public OrSpecification(ISpecification left, ISpecification right)
{
this._leftSpecification = left;
this._rightSpecification = right;
}
public override bool IsSatisfiedBy(T candidate)
{
return _leftSpecification.IsSatisfiedBy(candidate)
|| _rightSpecification.IsSatisfiedBy(candidate);
}
}
**public class NotSpecification : CompositeSpecification**
{
private readonly ISpecification _specification;
public NotSpecification(ISpecification specification)
{
this._specification = specification;
}
public override bool IsSatisfiedBy(T candidate)
{
return !_specification.IsSatisfiedBy(candidate);
}
}
```
接下来我们可以定义具体的规约模式,如果IdEqualSpecification、NameEqualSpecification规约等。下面就看下引入规约模式后,是如何解决上面仓储接口设计所存在的问题的。
```
// 引入规约模式,IProductRespository接口的定义
public interface IProductRespository
{
Product GetBySpecification(ISpecification spec);
IEnumerable FindBySpecification(ISpecification spec);
}
public class IdEqualSpecification : CompositeSpecification
{
private readonly Guid _id;
public IdEqualSpecification(Guid id)
{
_id = id;
}
public override bool IsSatisfiedBy(Product candidate)
{
return candidate.Id.Equals(_id);
}
}
public class NameEqualSpecification : CompositeSpecification
{
private readonly string _name;
public NameEqualSpecification(string name)
{
_name = name;
}
public override bool IsSatisfiedBy(Product candidate)
{
return candidate.Name.Equals(_name);
}
}
public class NewProductsSpecification : CompositeSpecification
{
public override bool IsSatisfiedBy(Product candidate)
{
return candidate.IsNew == true;
}
}
```
通过引入规约后,Product仓储中所有特定用途的操作都删除了,取而代之的是2个非常简洁的方法。规约模式解耦了仓储操作和筛选条件,如果业务扩展,我们可以定制我们的Specification,并将其注入到仓储即可。仓储的接口和实现无需任何修改。
下面通过一个具体的演示例子来看下传统规约模式的应用。具体的场景是这样的:我们想筛选一批int数组中的偶数和大于0的数字出来。因为这里涉及2个筛选条件,一个是偶数,一个是大于0的数,这样我们就可以通过定义偶数规约和正数规约。具体的实现如下所示:
```
// 具体规约,偶数规约
public class EvenSpecification : CompositeSpecification
{
public override bool IsSatisfiedBy(int candidate)
{
return candidate % 2 == 0;
}
}
// 具体的规约,正数规约
public class PlusSpecification : CompositeSpecification
{
public override bool IsSatisfiedBy(int candidate)
{
return candidate > 0;
}
}
```
接下来通过And操作和将2中规约组合起来形成新的规约。具体的测试代码如下所示:
```
using spec1 =SpecificationPatternDemo.Specification;
class Program
{
static void Main(string[] args)
{
Demo1();
Console.Read();
}
public static void Demo1()
{
var items = Enumerable.Range(-5, 10);
Console.WriteLine("产生的数组为:{0}", string.Join(", ", items.ToArray()));
spec1.ISpecification evenSpec = new spec1.EvenSpecification();
// 获得一个组合规约
var compositeSpecification = GetCompositeSpecification(evenSpec);
// 类似Where(it=>it%2==0 && it > 0)
// 前者是把两个条件合并写死成一个条件,而后者是将其组合成一个新条件。就如拼图出一架飞机和直接制造一个飞机模型概念是完全不同的
foreach (var item in items.Where(it=>compositeSpecification.IsSatisfiedBy(it)))
{
// 输出既是正数又是偶数的数
Console.WriteLine(item);
}
}
private static spec1.ISpecification GetCompositeSpecification(spec1.ISpecification spec)
{
spec1.ISpecification plusSpec = new spec1.PlusSpecification();
return spec.And(plusSpec);
}
}
```
具体的运行结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4b992ee.png)
上面我们已经介绍完规约模式的实现,并且通过对比的方式来介绍引入规约模式所解决之前的问题。但是传统规约模式的实现显得非常臃肿。因为你想实现一个新的规约,你需要新增一个新的Specification类,这样下来,我们的项目中必然会堆积大量的Specification类。有些规约可能只只使用了一次。这就好比.NET里的委托方法一样,为了解决类似的问题,.NET引入了匿名方法和lamada表达式。同样,我们借助C#的特性也可以使得传统规约模式的实现更轻量。下面就具体看下规约模式的轻量实现是如何去实现的。
## 五、规约模式的轻量实现
从上面可以看出,规约模式的关键在于IsSatisifiedBy函数,该函数用于校验某个对象是否满足该规约所表示的条件,IsSatisifiedBy函数的返回类型为bool类型,这样我们完全可以让一个ISpecification只具有IsSatisifiedBy函数。然后该函数返回一个委托调用结果。至于原本ISpecification中的And、Or和Not方法,我们将它们提起成扩展方法。经过上面的分析,轻量化后的规约模式实现也就出来了。具体实现如下所示:
```
public interface ISpecification
{
bool IsSatisfiedBy(T candidate);
}
public class Specification : ISpecification
{
private readonly Func _isSatisfiedBy;
public Specification(Func isSatisfiedBy)
{
this._isSatisfiedBy = isSatisfiedBy;
}
public bool IsSatisfiedBy(T candidate)
{
return _isSatisfiedBy(candidate);
}
}
public static class SpecificationExtensions
{
public static ISpecification And(this ISpecification left, ISpecification right)
{
return new Specification(candidate => left.IsSatisfiedBy(candidate) && right.IsSatisfiedBy(candidate));
}
public static ISpecification Or(this ISpecification left, ISpecification right)
{
return new Specification(candidate => left.IsSatisfiedBy(candidate) || right.IsSatisfiedBy(candidate));
}
public static ISpecification Not(this ISpecification one)
{
return new Specification(candidate => !one.IsSatisfiedBy(candidate));
}
}
```
使用扩展方法的好处在于,如果我们要加一个逻辑运行,如异或,那么就不需要修改接口了。修改接口是一个不推荐的的事情。因为接口修改会破坏之前已经发布的接口实现。因此,一旦接口发布之后,它就不能被修改了。这意味着,我们在定义接口时应该仔细推敲,做到接口的职责应该尤其单一。
轻量的实现使得使用Specification对象容易多了,我们不需要为每段逻辑创建一个独立的Specification类。下面具体看下规约模式的轻量实现的使用示例:
```
using SpecificationPatternDemo.Specification_2;
using spec2 = SpecificationPatternDemo.Specification_2;
class Program
{
static void Main(string[] args)
{
Demo2();
Console.Read();
}
public static void Demo2()
{
var items = Enumerable.Range(-5, 10);
Console.WriteLine("产生的数组为:{0}", string.Join(", ", items.ToArray()));
spec2.ISpecification evenSpec = new spec2.Specification(it => it % 2 == 0);
var compositeSpec = GetCompositeSpecification2(evenSpec);
foreach (var i in items.Where(it => compositeSpec.IsSatisfiedBy(it)))
{
Console.WriteLine(i);
}
}
private static spec2.ISpecification GetCompositeSpecification2(spec2.ISpecification spec)
{
spec2.ISpecification plusSpec = new spec2.Specification(it => it > 0);
return spec.And(plusSpec);
}
}
```
从上面的例子可以看出,此时并不需要定义单独的Specification对象了,只需要用委托来代替即可。其运行结果与上面传统实现一样。其实,还可以更简单,我们可以直接使用一个委托,而不不需要定义ISpecification接口和其Specification实现。其实现方式如下所示:
```
// 更轻量的实现
public static class SpecExtensitions
{
public static Func And(this Func left, Func right)
{
return candidate => left(candidate) && right(candidate);
}
public static Func Or(this Func left, Func right)
{
return candidate => left(candidate) || right(candidate);
}
public static Func Not(this Func one)
{
return candidate => !one(candidate);
}
}
```
上面的实现,我们就只需要一个扩展方式就可以了,其使用示例代码如下所示:
```
class Program
{
static void Main(string[] args)
{
Demo3();
Console.Read();
}
public static void Demo3()
{
var items = Enumerable.Range(-5, 10);
Console.WriteLine("产生的数组为:{0}", string.Join(", ", items.ToArray()));
Func evenSpec = it => it % 2 == 0;
var compositeSpec = GetCompositeSpec(evenSpec);
foreach (var i in items.Where(it => compositeSpec(it)))
{
Console.WriteLine(i);
}
}
private static Func GetCompositeSpec(Func spec)
{
return spec.And(it => it > 0);
}
}
```
## 六、规约模式的轻量实现的完善——对Linq查询支持
上面轻量级的Specification模式抛弃了具体的Specification类型,而是使用一个委托对象关键的IsSatisfiedBy方法。其优势在于使用简单,但是该实现不能支持Linq查询或表达式的场景。因为EF中的DbContext.Dbset集合的进行where筛选参数只能是表达式树。所以我们不能用委托对象来判断逻辑,取而代之的使用表达式树。对于表达式树的构造主要由参数和主体构造,所以针对于Not方法可以如下的方式来实现:
```
public static class SpecExprExtensions
{
public static Expression> Not(this Expression> one)
{
var candidateExpr = one.Parameters[0];
var body = Expression.Not(one.Body);
return Expression.Lambda>(body, candidateExpr);
}
}
```
对于Not方法,我们只要获取它的参数表达式,再将它的Body外包一个Not表达式,便可以此构造一个新的表达式了。但And和Or方法实现不能像Not一样简单处理:
```
// 不能这么处理
public static Expression> And(
this Expression> one, Expression> another)
{
var candidateExpr = one.Parameters[0];
var body = Expression.And(one.Body, another.Body);
return Expression.Lambda>(body, candidateExpr);
}
```
因为one和another两个表达式虽然都是同样的形式(Expression<Func<T, bool>>),但是它们的“参数”不是同一个对象。即one.Body和another.Body并没有公用一个ParameterExpression实例,于是无论采用哪个表达式的参数,在Expression.Lambda方法调用的时候,都会出现body中的某个参数对象并没有出现在参数列表中的错误。
既然参数不一致,所以要实现And和Or方法,必须统一两个表达式树的参数。为了达到这个目标,我们可以利用[ExpressionVisitor](https://msdn.microsoft.com/zh-cn/library/system.linq.expressions.expressionvisitor(v=vs.110).aspx)类来实现。这里定义一个派生于[ExpressionVisitor](https://msdn.microsoft.com/zh-cn/library/system.linq.expressions.expressionvisitor(v=vs.110).aspx)的类。具体实现如下:
```
internal class ParameterReplacer : ExpressionVisitor
{
public ParameterReplacer(ParameterExpression paramExpr)
{
this.ParameterExpression = paramExpr;
}
public ParameterExpression ParameterExpression { get; private set; }
public Expression Replace(Expression expr)
{
return this.Visit(expr);
}
protected override Expression VisitParameter(ParameterExpression p)
{
return this.ParameterExpression;
}
}
```
Expressionvistor可以用于求值、变形等各种操作。它提供了遍历表达式树的标准方式,如果你直接继承这个类并调用Visit方法(如上面Replace方法的实现一样),那么最终返回的结果便是传入的Expresssion参数本身。但是,如果你覆盖任意一个方法,返回了与传入时不同的对象,那么最终的结果就是一个新的Expression对象。就如上面VisitParameter方法实现一样。它直接返回我们定义的ParameterExpression对象。
通过上面分析,ParameterExpression类的作用是将一个表达式里的所有ParameterExpression替换成我们指定的新对象,这样就可以解决之前参数不一致的情况。所以我们And和Or方法的实现就是将两个表达式树参数替换成我们首先定义好的参数表达式。具体的实现方式如下所示:
```
public static class SpecExprExtensions
{
public static Expression> Not(this Expression> one)
{
var candidateExpr = one.Parameters[0];
var body = Expression.Not(one.Body);
return Expression.Lambda>(body, candidateExpr);
}
public static Expression> And(this Expression> one,
Expression> another)
{
// 首先定义好一个ParameterExpression
var candidateExpr = Expression.Parameter(typeof (T), "candidate");
var parameterReplacer = new ParameterReplacer(candidateExpr);
// 将表达式树的参数统一替换成我们定义好的candidateExpr
var left = parameterReplacer.Replace(one.Body);
var right = parameterReplacer.Replace(another.Body);
var body = Expression.And(left, right);
return Expression.Lambda>(body, candidateExpr);
}
public static Expression> Or(
this Expression> one, Expression> another)
{
var candidateExpr = Expression.Parameter(typeof (T), "candidate");
var parameterReplacer = new ParameterReplacer(candidateExpr);
var left = parameterReplacer.Replace(one.Body);
var right = parameterReplacer.Replace(another.Body);
var body = Expression.Or(left, right);
return Expression.Lambda>(body, candidateExpr);
}
}
```
到此,我们就完成了规约模式对Linq支持的轻量实现了。下面让我们看看上面轻量实现是如何调用的呢?具体调用代码如下:
```
class Program
{
private static void Main(string[] args)
{
Demo1();
Console.Read();
}
public static void Demo1()
{
var items = Enumerable.Range(-5, 10);
Console.WriteLine("产生的数组为:{0}", string.Join(", ", items.ToArray()));
Expression> f = i => i % 2 != 0;
f = f.Not().And(i => i > 0);
// 通过AsQueryable成IQueryable,因为IQueryable的Where方法的参数要求是表达式树
foreach (var i in items.AsQueryable().Where(f))
{
Console.WriteLine(i);
}
}
}
```
其运行结果与前面的例子中的运行结果一样,一样成功返回了即是偶数又是正数的集合。
## 七、总结
到这里,规约模式的实现就结束了,后期将会在网上书店的案例中引入规约模式,dax.net的Byteart Retail案例中规约模式的实现即包括了传统实现,也包括了对Linq支持的轻量实现。开始我认为传统实现是多余的,因为你已经有了规约模式的轻量实现了,何必又有传统实现呢?这不是包括两种实现吗?后面仔细想想,这样设计也有其存在的道理,因为对于一些逻辑复杂的规约实现,我们可以新建一个具体的规约类,但对于一些简单和仅使用一次的规约逻辑,就可以直接用表达式树来代替,就不需要单独为该段逻辑单独新建一个具体的规约类。这样的实现就如同,有了匿名方法和Lambda表达式,是不是委托就可以不需要了。显然不是的,所以我在我的网上书店案例中也将会引入这两种实现,让用户可以灵活选择这两种方式。在下一专题,我继续介绍一个前期准备的内容,即工作单元模式(Unit Of Work,即UOW)。
本专题的所有源码下载:[SpecificationPatternDemo.zip](http://files.cnblogs.com/files/zhili/SpecificationPatternDemo.zip)
';
.NET领域驱动设计实战系列 专题二:结合领域驱动设计的面向服务架构来搭建网上书店
最后更新于:2022-04-02 00:13:57
# [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店
## 一、前言
在前面专题一中,我已经介绍了我写这系列文章的初衷了。由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这对于一些刚刚接触领域驱动设计的朋友可能会非常迷茫,从而觉得领域驱动设计很难,很复杂,因为学习中要消化一个整个案例的知识,这样未免很多人消化不了就打退堂鼓,就不继续研究下去了,所以这样也不利于DDD的推广。然而本系列可以说是刚接触领域驱动设计朋友的福音,本系列将结合领域驱动设计的思想来一步步构建一个网上书店,从而让大家学习DDD不再枯燥和可以看到一个DDD案例的形成历程。最后,再DDD案例完成之后,将从中抽取一个领域驱动的框架,从而大家也可以看到一个DDD框架的形成历程,这样就不至于一下子消化一整个框架和案例的知识,而是一步步消化。接下来,该专题将介绍的是:结合领域驱动设计的SOA架构来构建网上书店,本专题中并没有完成网上书店的所有页面和覆盖DDD中的所有内容,而只是一部分,后面的专题将会在本专题的网上书店进行一步步完善,通过一步步引入DDD的内容和重构来完成整个项目。
## 二、DDD分层架构
从概念上说,领域驱动设计架构主要分为四层,分别为:基础设施层、领域层、应用层和表现层。
* 基础结构层:该层专为其他各层提供各项通用技术框架支持。像一些配置文件处理、缓存处理,事务处理等都可以放在这里。
* 领域层:简单地说就是业务所涉及的领域对象(包括实体、值对象)、领域服务等。该层就是所谓的领域模型了,领域驱动设计提倡是富领域模型,富领域模型指的是:尽量将业务逻辑放在归属于它的领域对象中。而之前的三层架构中的领域模型都是贫血领域模型,因为在三层中的领域模型只包含业务属性,而不包含任何业务逻辑。本专题的网上书店领域模型目前还没有包含任何业务逻辑,在后期将会完善。
**实体可以认为对应于数据库的表,而值对象一般定义在实体类中。**
* 应用层:该层不包含任何领域逻辑,它主要用来对任务进行协调,它构建了表现层和领域层的桥梁。SOA架构就是在该层进行实现的。
* 表现层:指的是用户界面,例如Asp.net mvc网站,WPF、Winform和控制台等。它主要用来想用户展现内容。
下面用一个图来形象展示DDD的分层架构:
![52029421305](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4ae9d0c.gif)
本系列介绍的领域驱动设计实战,则自然少了领域驱动设计分层架构的实现了,上面简单介绍了领域驱动的分层架构,接下来将详细介绍在网上书店中各层是如何去实现的。
## 三、网上书店领域模型层的实现
在应用领域驱动设计的思想来构建一个项目,则第一步就是了解需求,明白项目的业务逻辑,了解清楚业务逻辑后,则把业务逻辑抽象成领域对象,领域对象所放在的位置也就是领域模型层了。该专题介绍的网上书店主要完成了商品所涉及的页面,包括商品首页,单个商品的详细信息等。所以这里涉及的领域实体包括2个,一个是商品类,另外一个就是类别类,因为在商品首页中,需要显示所有商品的类别。在给出领域对象的实现之前,这里需要介绍领域层中所涉及的几个概念。
* 聚合根:聚合根也是实体,但与实体不同的是,聚合根是由实体和值对象组成的系统边界对象。举个例子来说,例如订单和订单项,根据业务逻辑,我们需要跟踪订单和订单项的状态,所以设计它们都为实体,但只有订单才是聚合根对象,而订单项不是,因为订单项只有在订单中才有意义,意思就是说:用户不能直接看到订单项,而是先查询到订单,然后再看到该订单下的订单项。所以聚合根可以理解为用户直接操作的对象。在这里商品类和类别类都是一个聚合根。
根据面向接口编程原则,我们在领域模型中应该定义一个实体接口和聚合根接口,而因为聚合根也是属于实体,所以聚合根接口继承于实体接口,而商品类和类别类都是聚合根,所以它们都实现聚合根接口。如果像订单项只是实体不是聚合根的类则实现实体接口。有了上面的分析,则领域模型层的实现也就自然出来了,下面是领域对象的具体实现:
```
// 商品类
public class Product : AggregateRoot
{
public string Name { get; set; }
public string Description { get; set; }
public decimal UnitPrice { get; set; }
public string ImageUrl { get; set; }
public bool IsNew{ get; set; }
public override string ToString()
{
return Name;
}
}
```
```
// 类别类
public class Category : AggregateRoot
{
public string Name { get; set; }
public string Description { get; set; }
public override string ToString()
{
return this.Name;
}
}
```
另外,领域层除了实现领域对象外,还需要定义仓储接口,而仓储层则是对仓储接口的实现。仓储可以理解为在内存中维护一系列聚合根的集合,而聚合根不可能一直存在于内存中,当它不活动时会被持久化到数据中。而仓储层完成的任务是持久化聚合根对象到数据或从数据库中查询存储的对象来重新创建领域对象。
仓储层有几个需要明确的概念:
1. 仓储里面存放的对象一定是聚合根,因为领域模型是以聚合根的概念去划分的,聚合根就是我们操作对象的一个边界。所以我们都是对某个聚合根进行操作的,而不存在对聚合内的值对象进行操作。因此,仓储只针对聚合根设计。
2. 因为仓储只针对聚合根设计,所以一个聚合根需要实现一个仓储。
3. 不要把仓储简单理解为DAO,仓储属于领域模型的一部分,代表了领域模型向外界提供接口的一部分,而DAO是表示数据库向上层提供的接口表示。一个是针对领域模型而言,而另一个针对数据库而言。两者侧重点不一样。
4. 仓储分为定义部分和实现部分,在领域模型中定义仓储的接口,**而在基础设施层实现具体的仓储**。这样做的原因是:由于仓储背后的实现都是在和数据库打交道,但是我们又不希望客户(如应用层)把重点放在如何从数据库获取数据的问题上,因为这样做会导致客户(应用层)代码很混乱,很可能会因此而忽略了领域模型的存在。所以我们需要提供一个简单明了的接口,供客户使用,确保客户能以最简单的方式获取领域对象,从而可以让它专心的不会被什么数据访问代码打扰的情况下协调领域对象完成业务逻辑。这种通过接口来隔离封装变化的做法其实很常见。由于客户面对的是抽象的接口并不是具体的实现,所以我们可以随时替换仓储的真实实现,这很有助于我们做单元测试。在本专题的案例中,我们把仓储层的实现单独从基础设施层拎出来了,作为一个独立的层存在。这也就是为什么DDD分层中没有定义仓储层啊,而本专题的案例中多了一个仓储层的实现。
5. 仓储在设计查询接口时,会经常用到规约模式(Specification Pattern)。本专题的案例中没有给出,这点将会在后面专题添加上去。
6. 仓储一般不负责事务处理,一般事务处理会交给“工作单元(Unit Of Work)”去处理,同样本专题也没有涉及工作单元的实现,这点同样会在后面专题继续完善。这里列出来让大家对后面的专题可以有个清晰的概念,而不至于是空穴来风的。
介绍完仓储之后,接下来就在领域层中定义仓储接口,因为本专题中涉及到2个聚合根,则自然需要实现2个仓储接口。根据面向接口编程原则,我们让这2个仓储接口都实现与一个公共的接口:IRepository接口。另外仓储接口还需要定义一个仓储上下接口,因为在Entity Framework中有一个[DbContex](https://msdn.microsoft.com/zh-cn/library/system.data.entity.dbcontext%28v=vs.113%29.aspx?f=255&MSPPError=-2147217396)类,所以我们需要定义一个EntityFramework上下文对象来对DbContex进行包装。也就自然有了仓储上下文接口了。经过上面的分析,仓储接口的实现也就一目了然了。
```
// 仓储接口
public interface IRepository
where TAggregateRoot :class, IAggregateRoot
{
void Add(TAggregateRoot aggregateRoot);
IEnumerable GetAll();
// 根据聚合根的ID值,从仓储中读取聚合根
TAggregateRoot GetByKey(Guid key);
}
```
这样我们就完成了领域层的搭建了,接下面,我们就需要对领域层中定义的仓储接口进行实现了。我这里将仓储接口的实现单独弄出了一个层,当然你也可以放在基础设施层中的Repositories文件夹中。不过我看很多人都直接拎出来的。我这里也是直接作为一个层。
## 四、网上书店Repository(仓储)层的实现
定义完仓储接口之后,接下来就是在仓储层实现这些接口,完成领域对象的序列化。首先是产品仓储的实现:
```
// 商品仓储的实现
public class ProductRepository : IProductRepository
{
#region Private Fields
private readonly IEntityFrameworkRepositoryContext _efContext;
#endregion
#region Public Properties
public IEntityFrameworkRepositoryContext EfContext
{
get { return this._efContext; }
}
#endregion
#region Ctor
public ProductRepository(IRepositoryContext context)
{
var efContext = context as IEntityFrameworkRepositoryContext;
if (efContext != null)
this._efContext = efContext;
}
#endregion
public IEnumerable GetNewProducts(int count = 0)
{
var ctx = this.EfContext.DbContex as OnlineStoreDbContext;
if (ctx == null)
return null;
var query = from p in ctx.Products
where p.IsNew == true
select p;
if (count == 0)
return query.ToList();
else
return query.Take(count).ToList();
}
public void Add(Product aggregateRoot)
{
throw new NotImplementedException();
}
public IEnumerable GetAll()
{
var ctx = this.EfContext.DbContex as OnlineStoreDbContext;
if (ctx == null)
return null;
var query = from p in ctx.Products
select p;
return query.ToList();
}
public Product GetByKey(Guid key)
{
return EfContext.DbContex.Products.First(p => p.Id == key);
}
}
```
接下来是类别仓储的实现:
```
// 类别仓储的实现
public class CategoryRepository :ICategoryRepository
{
#region Private Fields
private readonly IEntityFrameworkRepositoryContext _efContext;
public CategoryRepository(IRepositoryContext context)
{
var efContext = context as IEntityFrameworkRepositoryContext;
if (efContext != null)
this._efContext = efContext;
}
#endregion
#region Public Properties
public IEntityFrameworkRepositoryContext EfContext
{
get { return this._efContext; }
}
#endregion
public void Add(Category aggregateRoot)
{
throw new System.NotImplementedException();
}
public IEnumerable GetAll()
{
var ctx = this.EfContext.DbContex as OnlineStoreDbContext;
if (ctx == null)
return null;
var query = from c in ctx.Categories
select c;
return query.ToList();
}
public Category GetByKey(Guid key)
{
return this.EfContext.DbContex.Categories.First(c => c.Id == key);
}
}
```
由于后期除了实现基于EF仓储的实现外,还想实现基于MongoDb仓储的实现,所以在仓储层中创建了一个EntityFramework的文件夹,并定义了一个IEntityFrameworkRepositoryContext接口来继承于IRepositoryContext接口,由于EF中持久化数据主要是由DbContext对象来完成了,为了有自己框架模型,我在这里定义了OnlineStoreDbContext来继承DbContext,从而用OnlineStoreDbContext来对DbContext进行了一次包装。经过上面的分析之后,接下来对于实现也就一目了然了。首先是OnlineStoreDbContext类的实现:
```
public sealed class OnlineStoreDbContext : DbContext
{
#region Ctor
public OnlineStoreDbContext()
: base("OnlineStore")
{
this.Configuration.AutoDetectChangesEnabled = true;
this.Configuration.LazyLoadingEnabled = true;
}
#endregion
#region Public Properties
public DbSet Products
{
get { return this.Set(); }
}
public DbSet Categories
{
get { return this.Set(); }
}
// 后面会继续添加属性,针对每个聚合根都会定义一个DbSet的属性
// ...
#endregion
}
```
接下来就是IEntityFrameworkRepositoryContext接口的定义以及它的实现了。具体代码如下所示:
```
public class EntityFrameworkRepositoryContext : IEntityFrameworkRepositoryContext
{
// 引用我们定义的OnlineStoreDbContext类对象
public OnlineStoreDbContext DbContex
{
get { return new OnlineStoreDbContext(); }
}
}
```
这样,我们的仓储层也就完成了。接下来就是应用层的实现。
## 五、网上书店应用层的实现
应用层应用了面向服务结构进行实现,采用了微软面向服务的实现WCF来完成的。网上书店的整个架构完全遵循着领域驱动设计的分层架构,用户通过UI层(这里实现的是Web页面)来进行操作,然后UI层调用应用层来把服务进行分发,通过调用基础设施层中仓储实现来对领域对象进行持久化和重建。这里应用层主要采用WCF来实现的,其中引用了仓储接口。针对服务而言,首先就需要定义服务契约了,这里我把服务契约的定义单独放在了一个服务契约层,当然你也可以在应用层中创建一个服务契约文件夹。首先就去看看服务契约的定义:
```
// 商品服务契约的定义
[ServiceContract(Namespace="")]
public interface IProductService
{
#region Methods
// 获得所有商品的契约方法
[OperationContract]
IEnumerable GetProducts();
// 获得新上市的商品的契约方法
[OperationContract]
IEnumerable GetNewProducts(int count);
// 获得所有类别的契约方法
[OperationContract]
IEnumerable GetCategories();
// 根据商品Id来获得商品的契约方法
[OperationContract]
Product GetProductById(Guid id);
#endregion
}
```
接下来就是服务契约的实现,服务契约的实现我放在应用层中,具体的实现代码如下所示:
```
// 商品服务的实现
public class ProductServiceImp : IProductService
{
#region Private Fields
private readonly IProductRepository _productRepository;
private readonly ICategoryRepository _categoryRepository;
#endregion
#region Ctor
public ProductServiceImp(IProductRepository productRepository, ICategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
_productRepository = productRepository;
}
#endregion
#region IProductService Members
public IEnumerable GetProducts()
{
return _productRepository.GetAll();
}
public IEnumerable GetNewProducts(int count)
{
return _productRepository.GetNewProducts(count);
}
public IEnumerable GetCategories()
{
return _categoryRepository.GetAll();
}
public Product GetProductById(Guid id)
{
var product = _productRepository.GetByKey(id);
return product;
}
#endregion
}
```
最后就是创建WCF服务来调用服务契约实现了。创建一个后缀为.svc的WCF服务文件,WCF服务的具体实现如下所示:
```
// 商品WCF服务
public class ProductService : IProductService
{
// 引用商品服务接口
private readonly IProductService _productService;
public ProductService()
{
_productService = ServiceLocator.Instance.GetService();
}
public IEnumerable GetProducts()
{
return _productService.GetProducts();
}
public IEnumerable GetNewProducts(int count)
{
return _productService.GetNewProducts(count);
}
public IEnumerable GetCategories()
{
return _productService.GetCategories();
}
public Product GetProductById(Guid id)
{
return _productService.GetProductById(id);
}
}
```
到这里我们就完成了应用层面向服务架构的实现了。从商品的WCF服务实现可以看到,我们有一个ServiceLocator的类。这个类的实现采用服务定位器模式,关于该模式的介绍可以参考d[ax.net的服务定位器模式](http://www.cnblogs.com/daxnet/archive/2013/01/05/2846055.html)的介绍。该类的作用就是调用方具体的实例,简单地说就是通过服务接口定义具体服务接口的实现,将该实现返回给调用者的。这个类我这里放在了基础设施层来实现。目前基础设施层只有这一个类的实现,后期会继续添加其他功能,例如缓存功能的支持。
另外,在这里使用了Unity依赖注入容器来对接口进行注入。主要的配置文件如下所示:
```
```
## 六、基础设施层的实现
基础设施层在本专题中只包含了服务定位器的实现,后期会继续添加对其他功能的支持,ServiceLocator类的具体实现如下所示:
```
// 服务定位器的实现
public class ServiceLocator : IServiceProvider
{
private readonly IUnityContainer _container;
private static ServiceLocator _instance = new ServiceLocator();
private ServiceLocator()
{
_container = new UnityContainer();
_container.LoadConfiguration();
}
public static ServiceLocator Instance
{
get { return _instance; }
}
#region Public Methods
public T GetService()
{
return _container.Resolve();
}
public IEnumerable ResolveAll()
{
return _container.ResolveAll();
}
public T GetService(object overridedArguments)
{
var overrides = GetParameterOverrides(overridedArguments);
return _container.Resolve(overrides.ToArray());
}
public object GetService(Type serviceType, object overridedArguments)
{
var overrides = GetParameterOverrides(overridedArguments);
return _container.Resolve(serviceType, overrides.ToArray());
}
#endregion
#region Private Methods
private IEnumerable GetParameterOverrides(object overridedArguments)
{
var overrides = new List();
var argumentsType = overridedArguments.GetType();
argumentsType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToList()
.ForEach(property =>
{
var propertyValue = property.GetValue(overridedArguments, null);
var propertyName = property.Name;
overrides.Add(new ParameterOverride(propertyName, propertyValue));
});
return overrides;
}
#endregion
#region IServiceProvider Members
public object GetService(Type serviceType)
{
return _container.Resolve(serviceType);
}
#endregion
}
```
## 七、UI层的实现
根据领域驱动的分层架构,接下来自然就是UI层的实现了,这里UI层的实现采用Asp.net MVC 技术来实现的。UI层主要包括商品首页的实现,和详细商品的实现,另外还有一些附加页面的实现,例如,关于页面,联系我们页面等。关于UI层的实现,这里就不一一贴出代码实现了,大家可以在最后的源码链接自行下载查看。
## 八、系统总体架构
经过上面的所有步骤,本专题中的网上书店构建工作就基本完成了,接下来我们来看看网上书店的总体架构图(这里架构图直接借鉴了dax.net的图了,因为本系列文章也是对其Byteart Retail项目的剖析过程):
![](http://images.cnblogs.com/cnblogs_com/daxnet/201211/201211131559014210.png)
最后附上整个解决方案的结构图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4b08198.png)
## 九、网上书店运行效果
实现完之后,大家是不是都已经迫不及待地想看到网上书店的运行效果呢?下面就为大家来揭晓,目前网上书店主要包括2个页面,一个是商品首页的展示和商品详细信息的展示。首先看下商品首页的样子吧:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4b1be46.png)
图书的详细信息页面:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4b4bebf.png)
## 十、总结
到这里,本专题的内容就介绍完了, 本专题主要介绍面向领域驱动设计的分层架构和面向服务架构。然后结合它们在网上书店中进行实战演练。在后面的专题中我会在该项目中一直进行完善,从而形成一个完整了DDD案例。在接下来的专题会对仓储的实现应用规约模式,在应用之前,我会先写一个专题来介绍规约模式来作为一个准备工作。
GitHub 开源地址:[https://github.com/lizhi5753186/OnlineStore](https://github.com/lizhi5753186/OnlineStore)。
';
.NET领域驱动设计实战系列 专题一:前期准备之EF CodeFirst
最后更新于:2022-04-02 00:13:54
# [.NET领域驱动设计实战系列]专题一:前期准备之EF CodeFirst
## 一、前言
从去年已经接触领域驱动设计(Domain-Driven Design)了,当时就想自己搭建一个DDD框架,所以当时看了很多DDD方面的书,例如领域驱动模式与实战,领域驱动设计:软件核心复杂性应对之道和领域驱动设计C# 2008实现等书,由于当时只是看看而已,并没有在自己代码中进行实现,只是初步了解一些DDD分层的思想和一些基本概念,例如实体,聚合根、仓储等概念,今年有机会可以去试试面试一个架构岗位的时候,深受打击,当面试官问起是否在项目中使用过DDD思想来架构项目时,我说没有,只是了解它的一些基本概念。回来之后,便重新开始学习DDD,因为我发现做成功面试一个架构师,则必须有自己的一个框架来代表自己的知识体系,而不是要你明白这个基本概念。此时学习便决定一步步来搭建一个DDD框架。但是这次的过程并不是像dax.net那样,一开始就去搭建框架,然后再用一个实际的项目来做演示框架的使用。因为我觉得这样对于一些初学者来学习的话,难度比较大,因为刚开始写框架根本看到什么,而且看dax.net的Apworks框架很多代码也不明白他为什么这么写的,从框架代码并不能看出作者怎么一步步搭建框架的,读者只能一下子看到整个成型的框架,对于刚接触DDD的朋友难度非常大,以至于学习了一段时间的DDD之后,就放弃了。这个感觉本人学习过程中深有体会。所以本系列将直接把DDD的思想应用到一个实例项目中,完全实例项目后,再从中抽取一个DDD框架出来,并且会一步步介绍如何将DDD的思想应用到一个实际项目中(dax.net中ByteartRetail项目也是直接给出一个完整的DDD演示项目的,并没有记录搭建过程,同样对于读者学习难度很大,因为一下子来吸收整个项目的知识,接受不了,读者自然就心灰意冷,也就没有继续学习DDD的动力了)。本文并没有开始介绍DDD项目的实际实现,而是一个前期准备工作,因为DDD项目中一般会使用的实体框架来完成,作为.NET阵营的人,自然首先会使EntityFramework。下面就具体介绍下EF中code First的实现,因为在后面的DDD项目实现中会使用到EF的CodeFirst。
## 二、EF CodeFirst的实现步骤
因为我之前没怎么接触EF的CodeFirst实现,所以在看dax.net的ByteartRetail项目的时候,对EF仓储的实现有疑惑,所以去查阅相关EF的教程发现,原来应用了EF中的CodeFirst。所以把过程记录下来。下面就具体介绍下使用EF CodeFirst的具体实现步骤。
**步骤一:创建一个Asp.net MVC 4 Web项目,创建成功后,再添加一个Model类**
CodeFirst自然是先写实体类了,这里演示的是一个Book实体类,具体类的实现代码如下:
```
public class Book
{
public int BookId { get; set; }
public string BookName { get; set; }
public string Author { get; set; }
public string Publisher { get; set; }
public decimal Price { get; set; }
public string Remark { get; set; }
}
```
将使用这个类表示数据库中的一个表,每个Book类的实例对应数据库中的一行,Book类中的每个属性映射为数据库中的一列。
**步骤二:创建“BookDbContext”的类**
使用Nuget安装Entity Framework,安装成功后,在Models文件夹下新建一个“BookDbContext”的类,将类派生自“DbContext”类(命名空间为System.Data.Entity,dll在EntityFramework),具体BookDbContext的实现如下:
BookDbContext代表EF中Book在数据库中的上下文对象,通过DbSet<Book>使实体类与数据库关联起来。Books属性表示数据中的数据集实体,用来处理数据的存取与更新。
**步骤三:添加数据库连接**
在Web.config文件中,修改数据库连接字符串的配置,这里将数据库连接的name属性设置为BookDbContext,后面代码将会使用到该名字,并根据连接创建相应的数据库。
**步骤四:为Book创建控制器和Index视图**
首先创建一个控制器:在"Controlllers"上右键>添加>控制器,在打开的添加控制器对话框中,将控制器的名称改为"BookController",模板选择”空控制器“。修改BookController的代码为如下所示:
```
public class BookController : Controller
{
readonly BookDbContext _db = new BookDbContext();
//
// GET: /Book/
public ActionResult Index()
{
var books = from b in _db.Books
where b.Author == "Learninghard"
select b;
return View(books.ToList());
}
public ActionResult Create()
{
return View();
}
}
```
在Index方法内右键>"添加视图",在打开的”添加视图“对话框,勾选”创建强类型视图“,在模型类列表中选择”Book“(如果选择列表为空,则需要首先编译下项目),在支架模板列表中选择”List“,具体如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4a4a971.png)
点击添加按钮,VS为我们创建了Index.cshtml文件,修改Index.cshtml代码为如下所示的代码:
```
@model IEnumerable
@{
ViewBag.Title = "图书列表-EF CodeFirstImp";
}
```
编译并运行程序,在浏览器中输入地址:http://localhost:2574/Book,得到的运行结果如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4a5c970.png)
尽管没有数据,但EF已经为我们创建了相应的数据库了。此时在App_Data文件夹下生成了BookDB数据库,在解决方案点击选择所有文件,将BookDB数据库包括在项目中。
**步骤五:添加Create视图**
在BookController中的Create方法右键添加视图来添加Create视图,此时模型类仍然选择Book,但支架模板选择"Create"。添加成功后,VS会在Views/Book目录下添加一个Create.cshtml文件,由于这里选择了Create支架框架,所以VS会为我们生成一些默认的代码。在这个视图模板中,指定了强类型Book作为它的模型类,VS检查Book类,并根据Book类的属性,生成对应的标签名和编辑框,我们修改标签使其显示中文,修改会的代码如下所示:
```
@model EF_CodeFirstImp.Models.Book
@{
ViewBag.Title = "添加图书";
}
**
**
```
entityFramework节点是使用Nuget添加Entity Framework后自动添加的节点。下面测试下这种方案是否可以成功生成**BookDB_2**数据库呢?
运行项目,在浏览器中输入地址:http://localhost:2574/Book,显示界面成功后,你将在你的App_Data目录下看到如下截图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4ad7bcb.png)
从上图可以发现,这种方式同样成功生成了数据库。
## 三、总结
到这里,领域驱动设计实战系列的前期准备就结束了,本文主要介绍了如何使用EF CodeFirst的功能自动生成数据库、以及实体的添加、查看操作等。这里也简单介绍了MVC相关内容。下一专题将介绍如何利用DDD的思想来构建一个简单的网站,接下来的系列就逐一加入DDD的概念来对该网站进行完善。
本文所有源码下载:[EFCodeFirstImp.zip](http://files.cnblogs.com/files/zhili/EFCodeFirstImp.zip)
';
Index
@Html.ActionLink("添加图书", "Create")
图书名称 | 作者 | 出版社 | 价格 | 备注 | |
---|---|---|---|---|---|
@Html.DisplayFor(modelItem => item.BookName) | @Html.DisplayFor(modelItem => item.Author) | @Html.DisplayFor(modelItem => item.Publisher) | @Html.DisplayFor(modelItem => item.Price) | @Html.DisplayFor(modelItem => item.Remark) | @Html.ActionLink("编辑", "Edit", new { id=item.BookId }) | @Html.ActionLink("查看", "Details", new { id=item.BookId }) | @Html.ActionLink("删除", "Delete", new { id=item.BookId }) |
添加图书
@using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) }
@Html.ActionLink("Back to List", "Index")
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
```
分析上面的代码:
* @model EF_CodeFirstImp.Models.Book:指定该视图模板中的“模型”强类型化是一个Book类。
* @using (Html.BeginForm()){ }:创建一个Form表单,在表单中包含了对于Book类所生成的对应字段。
* @Html.EditorFor(model => model.BookName):根据模型生成模型中BookName的编辑控件(生成一个Input元素)
* @Html.ValidationMessageFor(model => model.BookName):根据模型生成模型中BookName的验证信息。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4a6efd6.png)
**步骤六:添加Create的Postback方法**
```
public class BookController : Controller
{
readonly BookDbContext _db = new BookDbContext();
//
// GET: /Book/
public ActionResult Index()
{
var books = from b in _db.Books
where b.Author == "Learninghard"
select b;
return View(books.ToList());
}
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Book book)
{
if (ModelState.IsValid)
{
_db.Books.Add(book);
_db.SaveChanges();
return RedirectToAction("Index");
}
else
return View(book);
}
}
```
这时,我们在添加图书界面中输入数据,并点击”添加“按钮时,数据库中就会添加一行记录。打开数据库,我们将看到如下截图的数据:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4a84268.png)
** 步骤七:设置视图模型的数据验证**
我们可以在模型类中显式地追加一个验证规则,使得对输入数据进行验证。修改之前的Book类为如下:
```
using System.ComponentModel.DataAnnotations; //需要额外添加该命名空间
public class Book
{
public int BookId { get; set; }
[Required(ErrorMessage = "必须输入图书名称")]
[StringLength(maximumLength: 100, MinimumLength = 1, ErrorMessage = "最多允许输入100个字符")]
public string BookName { get; set; }
[Required(ErrorMessage ="必须输入作者名称")]
public string Author { get; set; }
[Required(ErrorMessage ="必须输入出版社名称" )]
public string Publisher { get; set; }
public decimal Price { get; set; }
public string Remark { get; set; }
}
```
此时重新运行,并打开添加图书页面,当不输入任何数据的时候,点击”添加“按钮时,界面会出现一些提示信息,并阻止我们进行数据的提交操作,具体的结果界面如下所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4ac7fb3.png)
另外,EF创建数据库除了在第三步中添加连接字符串的方式外,还可以定义defaultConnectionFactory中添加parameters节点的方式来完成,dax.net中ByteartRetail项目中就是采用了这种方式。下面注释掉connectionStrings节点,在defaultConnectionFactory添加如下parameters节点:
```
.NET领域驱动设计实战系列
最后更新于:2022-04-02 00:13:52
# .NET领域驱动设计实战系列
';
跟我一起学WCF(13)——WCF系列总结
最后更新于:2022-04-02 00:13:50
# 跟我一起学WCF(13)——WCF系列总结
## 引言
WCF是微软为了实现SOA的框架,它是对微乳之前多种分布式技术的继承和扩展,这些技术包括Enterprise Service、.NET Remoting、XML Web Service、MSMQ等。WCF推出的原因在于:微软想将不同的分布式技术整合起来,提供一个统一的编程模型,这样对于开发者来说绝对是好事。在过去的2个月时间内,我陆续写了WCF系列文章,这些文章只是自己这段时间学习WCF内容的一个学习过程和笔记,希望通过这种写博文的方式记录下来和总结。本系列并没有对WCF机制做一个深入解析,只是讲解了WCF支持的功能和实现,关于更深入的了解,我相信,只有在项目中使用遇到问题和解决问题的方式才能更深入地理解,这系列文章只是想大家对WCF有一个全面的认识。下面是本系列文章的一个索引,希望可以帮助大家进行收藏,同时也帮助我自己索引。
### [第1篇] [跟我一起学WCF(1)——MSMQ消息队列](http://www.cnblogs.com/zhili/p/MSMQ.html)
MSMQ,Microsoft Message Queue——微软消息队列,它是微软之前实现分布式技术之一。其工作原理是:客户端将消息发送到一个消息队列中,服务从该消息队列中取出消息进行处理。通过消息队列的方式,把客户端和服务之间的耦合进行隔离,最明显的好处是异步和可离线功能,缺点是:由于客户端不直接把消息发送到服务进行处理,而是把消息发送到消息队列中,从而不适合客户端需要服务实时交互的情况下,大量请求的时候,响应可能延迟。
### [第2篇] [跟我一起学WCF(2)——利用.NET Remoting技术开发分布式应用](http://www.cnblogs.com/zhili/p/NETRemoting.html)
.NET Remoting是微软另一种分布式技术,WCF内部实现借鉴了该技术的实现。.NET Remoting优点可以实现跨应用程序域进行通信,缺点是不支持离线功能,并只适合.NET 平台的程序进行通信。其工作原理如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb46cfab9.gif)
### [第3篇] [跟我一起学WCF(3)——利用Web Services开发分布式应用](http://www.cnblogs.com/zhili/p/WebService.html)
XML Web Service是微软另外一种分布式技术,该技术具有的优点是跨平台,跨防火墙和自我描述,像MSMQ和.NET Remoting不能跨平台,因为其传输是二进制格式的数据,而XML Web Service传输的是基于XML的文本文件。其缺点是效率地和安全性,不适合做局域网内应用。所以,一般地说,局域网可以使用MSMQ和.NET Remoting技术,而基于Internet的应用使用XML Web Service。其实现原理如下图所示:
![XML Web services 生存期](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4724588.gif)
### [第四篇] [跟我一起学WCF(4)——第一个WCF程序](http://www.cnblogs.com/zhili/p/FirstWCF.html)
之前说过,WCF是对MSMQ、.NET Remoting、XML Web Service等技术的继承和扩展,所以利用WCF既可以做基于局域网的应用,也可以做基于互联网的分布式应用。WCF最重要的概念就是终结点,服务的提供者将服务通过一个或多个终结点进行发布给服务消费者。而终结点又由地址、绑定和契约组成。
这三个要素在WCF通信中起到的作用分别是:
* **地址(Address)**:地址标识了服务的位置,提供寻址的辅助信息和标识了服务的真实身份。Address解决了**Where the WCF service?**的问题。
* **绑定(Binding)**:绑定实现了通信的所有细节,包括网络传输,消息编码,以及其他为实现某种功能对消息进行的相应处理,例如安全、可靠传输和事务等功能。 WCF中具有一系列的系统已定义的绑定,如BasicHttpBinding、WsHttpBinding、NetTcpBinding等。Binding解决了How to Communicate with Service?的问题。
* **契约(Contract)**:契约是对服务操作的抽象,也是对消息交互模式以及消息结构的定义。WCF的契约大体可以分为两类,一类是对服务操作的描述;另一类是对数据的描述。服务契约(Service Contract)则属于对服务操作的描述,而后一类包括其余3中契约:数据契约(Data Contract)、消息契约(Message Contract)和错误契约(Fault Contract)。Contract解决了What function does the Service Provide?的问题。
后面的WCF文章都是对于这三个元素的扩展介绍。
### [第五篇] [跟我一起学WCF(5)——深入解析服务契约[上篇]](http://www.cnblogs.com/zhili/p/WCFServiceContract.html)
定义WCF服务,自然第一步就是需要定义服务契约,该博文主要介绍了WCF如何实现操作重载的。其主要实现逻辑是为相同的方法定义别名,使其生成的WSDL中operation标签不同。
### [第六篇] [跟我一起学WCF(6)——深入解析服务契约[下篇]](http://www.cnblogs.com/zhili/p/WCFServiceContract2.html)
WCF如果服务中定义了契约的继承关系,通过客户端生成的代理类不会生成具有继承关系的契约结构,解决这个问题的思路就是自定义代理类,使其具有和服务契约中一样的继承结构。
### [第七篇] [跟我一起学WCF(7)——WCF数据契约与序列化详解](http://www.cnblogs.com/zhili/p/WCFDataContract.html)
数据契约是定义服务和客户端之间要传送的自定义类型,对于一些基本类型如String、int等内置类型都是可序列化的,所以WCF默认对这些类型可进行序列化并进行传输,但对于自定义类、结构体等类型,因为这些类型默认不支持序列化,WCF中通过DataMemberAttribute特性是自定义类型可以进行序列化传输,并在服务中能进行反序列化为对象来进行数据的处理。WCF中默认使用的序列化器是[DataContractSerializer](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.datacontractserializer(v=vs.110).aspx)类。
### [第八篇] [跟我一起学WCF(8)——WCF中Session、实例管理详解](http://www.cnblogs.com/zhili/p/WCFInstanceManager.html)
WCF服务实例的管理借鉴了.NET Remoting技术的实现,同样有三种服务实例的激活方式:单调服务、会话服务和单例服务。
* **单调服务(Percall)**:为每个客户端请求分配一个新的服务实例。类似.NET Remoting中的SingleCall模式
* **会话服务(Persession)**:在会话期间,为每次客户端请求共享一个服务实例,类似.NET Remoting中的客户端激活模式。
* **单例服务(Singleton)**:所有客户端请求都共享一个相同的服务实例,类似于.NET Remoting的Singleton模式。但它的激活方式需要注意一点:当为对于的服务类型进行Host的时候,与之对应的服务实例就被创建出来,之后所有的服务调用都由这个服务实例进行处理。
WCF中服务激活的默认方式是PerSession,但不是所有的Bingding都支持Session,比如BasicHttpBinding就不支持Session。你也可以通过下面的方式使ServiceContract不支持Session。
### [第九篇] [跟我一起学WCF(9)——WCF回调操作的实现](http://www.cnblogs.com/zhili/p/WCFCallbackOperacation.html)
在WCF中,除了支持经典的请求/应答模式外,还提供了对单向操作、双向回调操作模式的支持,此外还有流操作的支持。本文介绍在WCF中回调操作的实现。
在WCF中,并不是所有的绑定都支持回调操作,只有具有双向能力的绑定才能够用于回调。例如,HTTP本质上是与连接无关的,所以它不能用于回调,因此我们不能基于basicHttpBinding和wsHttpBinding绑定使用回调,WCF为NetTcpBinding和NetNamedPipeBinding提供了对回调的支持,因为TCP和IPC协议都支持双向通信。为了让Http支持回调,WCF提供了WsDualHttpBinding绑定,它实际上设置了两个Http通道:一个用于从客户端到服务的调用,另一个用于服务到客户端的调用。
回调操作时通过回调契约来实现的,回调契约属于服务契约的一部分,一个服务契约最多只能包含一个回调契约。一旦定义了回调契约,就需要客户端实现回调契约。在WCF中,可以通过ServiceContract的[CallbackContract](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.servicecontractattribute.callbackcontract(v=vs.110).aspx)属性来定义回调契约。
### [第十篇] [跟我一起学WCF(10)——WCF中事务处理](http://www.cnblogs.com/zhili/p/WCFTransaction.html)
WCF支持事务的传递,事务的传递方式由绑定的事务流属性([TransactionFlow](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.nettcpbinding.transactionflow(v=vs.110).aspx)属性)、操作契约中的事务流选项([TransactionFlowOption](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.transactionflowoption(v=vs.110).aspx)) 以及操作行为特性中的事务范围属性([TransactionScopeRequired](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.operationbehaviorattribute.transactionscoperequired(v=vs.110).aspx))共同决定。WCF事务支持的四种传播模式是:Client/Service、Client、Service和None。下图是四种传播模式对应推荐的设置。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb49694ba.png)
### [第十一篇] [跟我一起学WCF(11)——WCF中队列服务详解](http://www.cnblogs.com/zhili/p/WCFMSMQ.html)
既然WCF对之前多种分布式技术的继承和扩展,所以也自然支持可离线的功能,该文介绍了WCF中对队列服务的支持和实现。其实现方式与MSMQ的实现方式类似,只是WCF为队列服务提供了新的API支持,主要通过[MsmqIntegrationBinding](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.msmqintegration.msmqintegrationbinding(v=vs.110).aspx)绑定类进行支持,其通信机制如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb49a1c68.png)
### [第十二篇] [跟我一起学WCF(12)——WCF中Rest服务入门](http://www.cnblogs.com/zhili/p/WCFRestService.html)
由Roy Thomas Fielding 在他的博士论文([“体系结构风格和基于网络软件体系的设计](http://ics.uci.edu/~fielding/pubs/dissertation/top.htm)”)中提出了Rest概念。Rest服务是将服务抽象为资源,每个资源都有一个唯一的统一资源标识符(URI),我们不再是通过调用操作的方式与服务进行交互了,而是通过HTTP标准动词(GET、POST、PUT和DELETE)的统一接口来完成。.NET 3.0之后,微软提供了新的API在WCF对Rest服务进行了支持,这些类包括WebHttpBinding类、WebGetAttribute、WebInvokeAttribute特性和WebServiceHost类。其实现方式和之前的WCF程序类似,只是使用新的API来对服务进行定义。
## 结束语:
到此,WCF系列也就告一段落了,通过对WCF技术系统的学习,我对WCF技术有了一个全面的认识,之后深入的理解就需要自己在项目中积累和实践了,希望通过这个系列也可以帮助到一些初学者。
';
跟我一起学WCF(12)——WCF中Rest服务入门
最后更新于:2022-04-02 00:13:48
# 跟我一起学WCF(12)——WCF中Rest服务入门
## 一、引言
要将Rest与.NET Framework 3.0配合使用,还需要构建基础架构的一些部件。在.NET Framework 3.5中,WCF在System.ServiceModel.Web组件中新增了编程模型和这些基础架构部件。
新编程模型有两个主要的新属性:[WebGetAttribute](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.web.webgetattribute(v=vs.110).aspx)和[WebInvokeAttribute](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.web.webinvokeattribute(v=vs.110).aspx),还有一个URI模板机制,帮助你声明每种方法响应使用的URI和动词。.NET Framework还提供了一个新的绑定(WebHttpBinding)和新的行为(WebHttpBehavior),此外,还提供了WebServiceHost和WebServiceHostFactory类来对Rest服务进行支持。下面让我们具体看看WCF目前对Rest服务的支持和实现。
## 二、REST服务是什么
对于这个问题,百度下有很多答案,这里给出百度百科中一个详细介绍的链接:[Rest服务](http://baike.baidu.com/view/1077487.htm?fr=aladdin)。我的理解的Rest服务是:以前我们都是把WCF服务抽象为操作的概念,而Rest最早是由Roy Thomas Fielding 在他的博士论文([“体系结构风格和基于网络软件体系的设计](http://ics.uci.edu/~fielding/pubs/dissertation/top.htm)”)中提出的。Rest服务是将服务抽象为资源,每个资源都有一个唯一的统一资源标识符(URI),我们不再是通过调用操作的方式与服务进行交互了,而是通过HTTP标准动词(GET、POST、PUT和DELETE)的统一接口来完成。总之一句话概括,Rest服务换了一种思维方式,把服务也当成一种资源,通过Get、Post、Put和Delete这些HTTP动词来进行交互。这样,问题就来了,Rest服务具有什么好处呢?即我们为什么要去关注Rest和实现它呢?
Rest优势就在于其使用极其简单,Rest服务要求很少的编码工作量,可以减少很多不必要的工作。Rest服务主要有以下优点:
* 无需引入SOAP消息传输层,轻量级和高效率的HTTP格式可直接被应用。
* 可以轻易地在任何编程语言中实现,尤其是在JS中。使用SOAP的服务与JS交互非常繁琐,而使用Rest服务与JS交互非常简单。
* 可以不使用任何编程语言就能访问服务,而只需要使用Web浏览器即可。
* 更好的性能和缓存支持。使用Rest服务可以改善响应时间和改善用户体验。
* 可扩展性和无状态性。Rest服务基于HTTP协议,每个请求都是独立的,一旦被调用,服务器不保留任何会话,这样可以更具响应性,通过减少通讯状态的维护工作来提供服务的可扩展性。
## 三、WXF和Asp.net Web API的比较
WCF是微软为生成面向服务的应用程序而提供的统一编程模型。[Asp.net Web API](http://www.asp.net/web-api) 是一个用来方便生成HTTP服务的框架,这些服务可供广泛的客户端访问,包括浏览器和移动设备。Asp.net Web API用于在.NET平台上生成Restful应用程序的理想平台。到这里问题又来了,Rest服务与SOAP服务的区别又是什么呢?
Rest相对于SOAP服务使用更加简单,对于开发者来说,学习成本较低,而SOAP作为一种古老的Web服务技术,近期内还不回退出历史舞台,而且随着SOAP 1.2的出现,SOAP 1.1中的一些缺点已得到改进。
REST目前只基于HTTP和HTTPS协议,而SOAP可支持任何传输协议,包括HTTP/HTTPS、TCP、SMTP等协议。另外Rest服务除了能使用XML作为数据承载外,还有JSON,RSS和[ATOM](http://baike.baidu.com/subview/21936/7031123.htm?fr=aladdin)形式。
Rest与SOAP对应的比较如下所示:
1. SOAP是一种工业标准,面对的应用需求时RPC(远程过程调用),而Rest面对的应用需求是分布式Web系统。
2. Rest服务强调数据,请求和响应消息都是数据的封装,而SOAP服务更强调接口,SOAP消息封装的是过程调用。Rest是面向资源的,而SOAP是面向接口的。
3. Rest架构下,HTTP是承载协议,也是应用协议,而SOAP架构下,HTTP只是承载协议,SOAP才是应用协议。
那在什么情况下使用Rest,什么情况下使用SOAP呢?这要看具体的实际情况。具体应用场景如下所示:
* 远程调用用SOAP。如果服务是作为一种功能提供,客户端调用服务是为了执行一个功能,用SOAP,比如常见的需求是认证授权。而数据服务用Rest。
* 要更多的考虑非功能需求时使用SOAP,如需考虑安全、传输和协议等需求的情况下。
* 低带宽、客户端的处理能力受限的场合可以考虑使用Rest。如在PDA或手机上消费服务。
介绍了Rest与SOAP的区别之后,让我们回到WCF与Asp.net Web API的比较上来,具体两者功能之间的对比如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4a0059a.png)
使用 WCF 可创建可靠、安全的 Web 服务,这些服务可通过各种传输方式来访问。 使用 ASP.NET Web API 可创建基于 HTTP 的服务,这些服务可从各种客户端来访问。 如果要创建和设计新的 REST 样式服务,请使用 ASP.NET Web API。 虽然 WCF 针对编写 REST 样式服务提供了一些支持,但 ASP.NET Web API 中的 REST 支持更加完整,并且,所有将来的 REST 功能改进都将在 ASP.NET Web API 中进行。
## 四、WCF中实现Rest服务
WCF 3.5中对Rest服务也做了支持,主要提供了[WebHttpBinding](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.webhttpbinding(v=vs.110).aspx)来对Rest进行支持,下面我们通过一个具体的实例来看看如何在WCF中实现一个Rest服务。我们还是按照之前三个步骤来创建该实例。
第一步:创建Rest服务接口和实现。具体的实现代码如下所示。
服务契约代码如下所示:
```
1 [ServiceContract(Namespace ="http://www.cnblogs.com/zhili/")]
2 public interface IEmployees
3 {
4 // 契约操作不再使用操作契约的方式来标识,而是使用WebGetAttribute特性来标识,从而表明该服务是Rest服务
5 [WebGet(UriTemplate = "all")]
6 IEnumerable GetAll();
7
8 [WebGet(UriTemplate = "{id}")]
9 Employee Get(string id);
10
11 [WebInvoke(UriTemplate="/", Method="PUT")]
12 void Create(Employee employee);
13
14 [WebInvoke(UriTemplate = "/", Method = "POST")]
15 void Update(Employee employee);
16
17 [WebInvoke(UriTemplate = "/", Method = "DELETE")]
18 void Delete(string id);
19 }
20
21 [DataContract(Namespace = "http://www.cnblogs.com.zhili/")]
22 public class Employee
23 {
24 [DataMember]
25 public string Id { get; set; }
26
27 [DataMember]
28 public string Name { get; set; }
29
30 [DataMember]
31 public string Department { get; set; }
32
33 [DataMember]
34 public string Grade { get; set; }
35
36 public override string ToString()
37 {
38 return string.Format("ID: {0,-5}姓名:{1,-5}部门:{2,-5}级别:{3}",Id, Name, Department, Grade);
39 }
40 }
```
从上面代码可以看出,Rest服务不再使用OperactionContract的方式来标识操作了,此时在Rest架构下,每个操作都被当做一种资源对待,所以这里的操作使用了WebGetAttribute特性和WebInvokeAttribute来标识。下面具体看看契约的具体实现,即Rest服务的实现代码。
```
1 namespace WCFContractAndService
2 {
3 public class EmployeesService : IEmployees
4 {
5 private static IList employees = new List
6 {
7 new Employee{ Id = "0001", Name = "LearningHard", Department = "开发部",Grade = "G6"},
8 new Employee{Id = "0002", Name = "张三", Department = "QA", Grade = "G5"}
9 };
10
11 public Employee Get(string id)
12 {
13 Employee employee = employees.FirstOrDefault(e => e.Id == id);
14 if (null == employee)
15 {
16 WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.NotFound;
17 }
18 return employee;
19 }
20
21 public IEnumerable GetAll()
22 {
23 return employees;
24 }
25
26 public void Create(Employee employee)
27 {
28 employees.Add(employee);
29 }
30
31
32 public void Update(Employee emp)
33 {
34 this.Delete(emp.Id);
35 employees.Add(emp);
36 }
37
38 public void Delete(string id)
39 {
40 Employee employee = this.Get(id);
41 if (null != employee)
42 {
43 employees.Remove(employee);
44 }
45 }
46 }
47 }
```
第二步:实现Rest服务宿主。这里还是使用控制台程序来作为宿主程序,主要的实现代码如下所示:
```
namespace RestServiceHost
{
class Program
{
static void Main(string[] args)
{
// Rest服务使用WebServiceHost类来为服务提供宿主
using (WebServiceHost webHost = new WebServiceHost(typeof(EmployeesService)))
{
webHost.Opened += delegate
{
Console.WriteLine("Rest Employee Service 开启成功!");
};
webHost.Open();
Console.Read();
}
}
}
}
```
对应的配置文件内容如下所示:
```
```
第三步:实现Rest服务调用客户端。这里通过通道工厂的方式来创建代理对象的,具体的实现代码如下所示:
```
1 namespace WCFClient
2 {
3 class Program
4 {
5 static void Main(string[] args)
6 {
7 using (ChannelFactory channelFactory = new ChannelFactory("employeeService"))
8 {
9 // 创建代理类
10 IEmployees proxy = channelFactory.CreateChannel();
11
12 Console.WriteLine("所有员工信息:");
13
14 // 通过代理类来对Rest服务进行操作
15 Array.ForEach(proxy.GetAll().ToArray(), emp => Console.WriteLine(emp.ToString()));
16
17 Console.WriteLine("\n添加一个新员工{0003}:");
18 proxy.Create(new Employee
19 {
20 Id = "0003", Name="李四", Department="财务部", Grade="G7"
21 });
22
23 Array.ForEach(proxy.GetAll().ToArray(), emp => Console.WriteLine(emp.ToString()));
24
25 Console.WriteLine("\n修改员工(0003)信息:");
26 proxy.Update(new Employee
27 {
28 Id = "0003", Name="李四", Department = "销售部", Grade ="G8"
29 });
30 Array.ForEach(proxy.GetAll().ToArray(), emp => Console.WriteLine(emp.ToString()));
31 Console.WriteLine("\n删除员工(0002)信息:");
32 proxy.Delete("0002");
33 Array.ForEach(proxy.GetAll().ToArray(), emp => Console.WriteLine(emp.ToString()));
34
35 Console.Read();
36 }
37 }
38 }
39 }
```
客户端对应的配置文件内容如下所示:
```
** **
```
经过上面的三步,Rest服务的构建工作就完成了,下面看看该程序的运行结果。
首先以管理员权限运行服务宿主程序,运行成功后的结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4a15e8d.png)
然后再运行客户端程序,运行成功后的结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4a26c54.png)
## 五、总结
到这里,本文要分享的内容就结束了,同时这也是WCF系列的最后一篇博文。WCF主要通过提供几个新的API来对Rest服务的实现,这里包括WebHttpBinding类、WebGetAttribute、WebInvokeAttribute特性和WebServiceHost类等。接下来一篇博文将对本系列做一个总结。
本文所有源码:[WCFRestService.zip](http://files.cnblogs.com/zhili/WCFRestService.zip)
';
跟我一起学WCF(11)——WCF中队列服务详解
最后更新于:2022-04-02 00:13:45
# 跟我一起学WCF(11)——WCF中队列服务详解
## 一、引言
在前面的WCF服务中,它都要求服务与客户端两端都必须启动并且运行,从而实现彼此间的交互。然而,还有相当多的情况希望一个面向服务的应用中拥有离线交互的能力。WCF通过服务队列的方法来支持客户端和服务之间的离线工作,客户端将消息发送到一个队列中,再由服务对它们进行处理。下面让我们具体看看WCF中的队列服务。
## 二、WCF队列服务的优势
在介绍WCF队列服务之前,首先需要了解微软消息队列(MSMQ)。MSMQ是在多个不同应用之间实现相互通信的一种异步传输模式,相互通信的应用可以分布在同一台机器,也可以分布在相连的网络环境。它的实现原理是:客户端将消息发送到一个容器中,然后把它保存到一个系统公用空间的消息队列(Message Queue)中,本地或异地的服务再从该队列中取出发送给它的消息进行处理。更多详细内容可以参考我的博文:[跟我一起学WCF(1)——MSMQ消息队列](http://www.cnblogs.com/zhili/p/MSMQ.html)。
WCF框架对MSMQ进行了集成和扩展,MSMQ支持离线消息模式,并且在WCF框架下,提供了基于http桥的internet网络队列服务的调用扩展。从而赋予了WCF队列服务以下几点优势:
1. 支持离线消息模式。因为WCF框架集成了MSMQ,所以WCF队列服务自然也支持离线消息。
2. 支持将操作分解。WCF支持将工作分解为多个操作放入队列中,可改善系统的可用性和吞吐量。
3. 提供对失败的事务做善后处理。当我们的业务事务需要几个小时或几天完成的时候,我们通常将它分为至少2个事务。第一个事务将需要立即完成的工作放入队列,而第二个事务用于验证第一个事务是否成功,并在必要的情况下对失败的事务进行善后处理。
4. 支持负载平衡。可以把过载的客户端请求放入队列,空闲的时候进行处理,这样可以平衡系统的吞吐量,改善性能。
## 三、WCF队列服务通信框架
WCF使用NetMsmqBinding来支持消息队列通信。当客户端调用服务时,客户端消息会被封装为MSMQ消息,发送到系统公用的消息队列中,服务宿主在运行状态下会启动通道监听器来检测消息队列消息,如果发现对应的消息,则会从队列里取出消息,使用分发器转发给对应的服务,具体的通信框架如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb49a1c68.png)
如果宿主离线,消息会被放入队列,等待下一次宿主联机时,在执行消息分发给指定WCF服务处理。
另外WCF还提供了[MsmqIntegrationBinding](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.msmqintegration.msmqintegrationbinding(v=vs.110).aspx)类,该类用于需要将WCF 应用和现有的基于MSMQ的应用集成的情况。WCF应用可利用该绑定向现有的MSMQ应用程序发生消息,或从这些应用程序接收消息。
## 四、利用WCF队列服务来实现离线操作
前面介绍WCF队列服务的优势和它的通信框架,下面具体通过一个例子来诠释WCF队列服务的实现。我们还是按照前面文章介绍的三个步骤来实现该实例。
第一步:定义契约和实现服务。具体的实现代码如下所示:
```
1 [ServiceContract]
2 public interface IWCFMSMQService
3 {
4 // 操作契约,必须为单向操作
5 [OperationContract(IsOneWay = true)]
6 void SayHello(string message);
7 }
8
9 // 契约实现
10 public class WCFMSMQService : IWCFMSMQService
11 {
12 public WCFMSMQService()
13 {
14 Console.WriteLine("WCF MSMQ Service instance was created at: {0}", DateTime.Now);
15 }
16
17 #region IOrderProcessor Members
18
19 [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
20 public void SayHello(string message)
21 {
22 Console.WriteLine("Hello! {0},调用WCF操作的时间为:{1}", message, DateTime.Now);
23 }
24
25 #endregion
26 }
```
上面代码需要注意一点:WCF操作必须定义为单向操作,因为要实现的是一个队列服务,其特点为异步、离线,无返回值。所以要设置IsOneWay属性为true。
第二步:实现宿主。这里仍然使用控制台应用程序作为WCF队列服务的宿主,具体的实现代码如下所示:
```
1 namespace WCFConsoleHost
2 {
3 class Program
4 {
5 static void Main(string[] args)
6 {
7 string path = @".\private$\LearningHardWCFMSMQ";
8 if (!MessageQueue.Exists(path))
9 {
10 MessageQueue.Create(path, true);
11 }
12
13 using (ServiceHost host = new ServiceHost(typeof(WCFMSMQService)))
14 {
15 host.Opened += delegate
16 {
17 Console.WriteLine("Service has begun to listen\n\n");
18 };
19
20 host.Open();
21
22 Console.Read();
23 }
24 }
25 }
26 }
```
对应的配置信息如下所示:
```
```
第三步:WCF客户端的实现。首先以管理员权限启动宿主,然后通过添加服务引用的方式来生成代理客户端类,具体在添加服务引用窗口地址栏输入:http://localhost:9003/mex来添加服务引用,添加服务引用成功后将生成代理类,通过代理类来对WCF服务进行访问,具体的实现代码如下所示:
```
1 namespace WCFClient
2 {
3 class Program
4 {
5 static void Main(string[] args)
6 {
7 WCFMSMQServiceClient proxy = new WCFMSMQServiceClient("NetMsmqBinding_IWCFMSMQService");
8 using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required))
9 {
10 Console.WriteLine("WCF First Call at:{0}", DateTime.Now);
11 proxy.SayHello("World");
12
13 Thread.Sleep(2000);//客户端休眠两秒,继续下一次调用
14 Console.WriteLine("WCF Second Call at:{0}", DateTime.Now);
15 proxy.SayHello("Learning Hard");
16
17 scope.Complete();
18 }
19
20 Console.Read();
21 }
22 }
23 }
```
经过上面三步,我们就完成了一个WCF队列服务程序。下面让我们看看该程序的运行结果。
因为WCF队列服务是对MSMQ的集成和扩展,所以此时该程序客户端可以在WCF服务离线的情况也可运行,即WCF服务宿主不启动,客户端也可以正常运行,这点与前面介绍的WCF程序完成不同,因为前面的WCF程序,如果宿主程序不启动而直接启动客户端程序,则客户端程序运行时将会发生异常。该程序之所以不会发生异常的原因在于,此时客户端程序是直接与消息队列进行交互的,而不是直接与WCF服务进行交互。此时只运行WCF客户端,你将看到如下图所示的运行结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb49bb826.png)
同时,你在消息队列的专有队列的队列消息中将看到两条未处理的消息信息,具体效果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb49cba29.png)
因为WCF服务宿主还没有启动,所以客户端发送的消息信息还没有被取出处理,接下来,我们启动下服务宿主来对队列中的消息进行处理,此时你可以选择关闭客户端(当然你也可以不关闭)。具体的WCF服务宿主的运行结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb49dfeba.png)
此时,如果刷新消息队列的专有队列中的队列消息,你将看不到任何消息了,这是因为WCF服务从消息队列取出消息进行处理了,即消息完成了出队的操作。
## 五、总结
到这里,WCF队列服务的介绍也就结束了。本文主要介绍了WCF集成了MSMQ的功能,WCF可以利用netMsmqBinding来实现离线服务。其实现程序与MSMQ程序实现差不多,关于MSMQ的相关内容也可以参考我的另一篇博文:[跟我一起学WCF(1)——MSMQ消息队列](http://www.cnblogs.com/zhili/p/MSMQ.html)。在下一篇博文中将分享WCF对Rest服务的支持和实现。
本文所有源码:[WCFMSMQService.zip](http://files.cnblogs.com/zhili/WCFMSMQService.zip)
';
跟我一起学WCF(10)——WCF中事务处理
最后更新于:2022-04-02 00:13:43
# 跟我一起学WCF(10)——WCF中事务处理
## 一、引言
好久没更新,总感觉自己欠了什么一样的,所以今天迫不及待地来更新了,因为后面还有好几个系列准备些,还有很多东西需要学习总结的。今天就来介绍下WCF对事务的支持。
## 二、WCF事务详解
## 2.1 事务概念与属性
首先,大家在学习数据库的时候就已经接触到事务这个概念了。所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单元。例如,银行转账功能,这个功能涉及两个逻辑操作
1. 从一个账户A中扣钱
2. 另一个账户B增加对应的钱。
现实生活中,这两个操作需要要么都执行,要么都不执行。所以在实现转账功能时,这两个操作就可以作为一个事务来进行提交,这样才能够保证转账功能的正确执行。
上面通过银行转账的例子来解释了事务的概念了,也可以说非常容易理解。然后在数据库的相关书籍里面都会介绍事务的特性。一个逻辑工作单元要成为事务,必须满足四个特性,这四个特性包括原子性、一致性、隔离性和持久性。这四个特性也简称为ACID(ACID是四个特性英文单词首字母的缩写)。
* 原子性。此属性可确保特定事务下完成的所有更新都已提交并保持持久,或所有这些更新都已中止并回滚到其先前状态。
* 一致性。此属性可保证某一事务下所做的更改表示从一种一致状态转换到另一种一致状态。例如,将钱从支票帐户转移到存款帐户的事务并不改变整个银行帐户中的钱的总额。
* 隔离。此属性可防止事务遵循属于其他并发事务的未提交的更改。隔离在确保一种事务不能对另一事务的执行产生意外的影响的同时,还提供一个抽象的并发。
* 持久性。这意味着一旦提交对托管资源(如数据库记录)的更新,即使出现失败这些更新也会保持持久。
## 2.2 事务协议
WCF支持分布式事务,也就是说WCF中的事务可以跨越服务边界、进程、机器和网络,在多个客户端和服务之间存在。即WCF中事务可以被传播的。既然WCF支持事务,则自然就有对应传输事务信息的相关协议。所以也就有了事务协议。
WCF使用不同的事务协议来控制事务的执行范围,事务协议是为了实现分布式环境中事务的传播。WCF支持以下三种事务协议:
1. 轻量级协议(Lightweight Protocol):该协议仅用于管理本地环境中的事务,即处于同一应用程序域中的事务。它无法跨越应用程序域的边界传播事务,则更不用说跨越进程和机器边界了。所以Lightweight Protocol只适用于某个服务的内部或外部。但相对于其他协议来说,轻量级协议的性能是最好的,这应该是显然的,不能跨越进程和机器边界,则就不存在网络传输。
2. OleTx协议:该协议用于跨应用程序域、进程和机器边界传播事务。协议采用远程过程调用(RPC),并采用Windows专用的二进制格式。但该协议无法穿越防火墙或与非Windows方协作。所以OleTx协议多用于Windows体系下的内网环境(即Intranet环境)。
3. WS-Atomic(WS原子性,WSAT)事务协议:该协议与OleTx协议类似,同样允许事务穿越应用程序域、进程和机器边界传播事务。但不同于OleTx协议的是,WSAT协议基于一种行业标准,它使用HTTP协议,并编码形式为文本格式,因而可以穿越防火墙。虽然可以在内网中使用WSAT协议,但它主要还是用于Internet环境。
因为轻量级协议不能跨越服务边界传播事务,所有没有绑定支持轻量级协议。WCF预定义的绑定中实现了标准的WS-Atomic 协议和Microsoft专有的OleTx协议,我们可以通过编程或配置文件来设置事务协议。具体设置方法如下所示:
```
1
2
3
4
5
6
7 // 通过编程设置
8 NetTcpBinding tcpBinding = new NetTcpBinding();
9 // 注意: 事务协议的配置只有在事务传播的情况下才有意义
10 tcpBinding.TransactionFlow = true;
11 tcpBinding.TransactionProtocol = TransactionProtocol.WSAtomicTransactionOctober2004;
```
这里需要注意,事务协议的配置只有在允许事务传播的情况下才有意义。并且NetTcpBinding和NetNamedPipeBinding都提供了TransactionProtocol属性。由于TCP和IPC绑定只能在内网使用,将它们设置为WSAT协议并无实际意义,对于WS绑定(如WSHttpBinding、WSDualHttpBinding和WSFederationHttpBinding)并没有TransactionProtocol属性,它们设计的目的在于当涉及多个使用WAST协议的事务管理器时,能够跨越Internet。但如果只有一个事务协调器,OleTx协议将是默认的协议,不必也不能为它配置一个特殊的协议。
## 2.3 事务管理器
分布式事务的实现要依靠第三方事务管理器来实现。它负责管理一个个事务的执行情况,最后根据全部事务的执行结果,决定提交或回滚整个事务。WCF提供了三个不同的事务管理器,它们分别是轻量级事务管理器(LTM)、核心事务管理器(KTM)和分布式事务协调器(DTC)。WCF根据平台使用的公共,应用程序的事务执行的任务、调用的服务以及所消耗的资源分配合适的事务管理器。通过自动地分配事务管理器,WCF将事务管理从服务代码和用到的事务协议中解耦出来,开发者不必为事务管理器而苦恼。下面分别介绍下这三种事务管理器。
* LTM:它只管理本地事务,即在一个单独应用程序域内的事务。LTM只能管理在一个单独服务内的事务,该服务不能将事务传递给其他服务。LTM在所有的事务管理器中,性能最好。
* KTM:与LTM一样,KTM管理的事务只能引入一个服务,并且该服务不得向其他服务传播事务。
* DTC:DTC可以管理跨越任意执行边界的事务,从本地跨越所有的边界,如进程、机器或站点的边界。DTC既可以使用OleTx协议,也可以使用WSAT协议。DTC与WCF紧密的集成一起,它是每个运行WCF的机器上默认可用的系统服务,DTC可以创建新的事务、跨机器传播事务,手机之一管理器的投票并通知资源管理器进行回滚或提交。
## 2.4 服务支持的4种事务模式
事务使用哪个事务由绑定的事务流属性([TransactionFlow](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.nettcpbinding.transactionflow(v=vs.110).aspx)属性)、操作契约中的事务流选项([TransactionFlowOption](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.transactionflowoption(v=vs.110).aspx)) 以及操作行为特性中的事务范围属性([TransactionScopeRequired](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.operationbehaviorattribute.transactionscoperequired(v=vs.110).aspx))共同决定。TransactionFlow属性有2个值,true 或false,TransactionFlowOption有三个值,NotAllowed、Allowed和Mandatory,TransactionScopeRequired有两个值,true或false。所以一共有12种(2*3*2)可能的配置设置。在这些配置设置中,有4种不满足要求的,例如在绑定中设置TransactionFlow属性为false,却设置TransactionFlowOption为Mandatory。下图列出了剩下的8种情况:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb49694ba.png)
上图中的8中排列组合实际最终只产生了四种事务传播模式,这4种传播模式为:Client/Service、Client、Service和None。上图黑体字指出各种模式推荐的配置设置。在设计应用程序时,每种模式都有它自己的适用场景。对于除None模式的其他三种模式的推荐配置详细介绍如下所示:
* Client/Service:最常见的一种事务模型,通常由客户端或服务本身启用一个事务。设置步骤:
(1) 选择一个支持事务的Binding,设置 TransactionFlow = true。 (2) 设置 TransactionFlow(TransactionFlowOption.Allowed)。 (3) 设置 OperationBehavior(TransactionScopeRequired=true)。
* Client:强制服务必须参与事务,而且必须是客户端启用事务。设置步骤:
(1) 选择一个支持事务的Binding,设置 TransactionFlow = true。 (2) 设置 TransactionFlow(TransactionFlowOption.Mandatory)。 (3) 设置 OperationBehavior(TransactionScopeRequired=true)。
* Service:服务必须启用一个根事务,且不参与任何外部事务。设置步骤:
(1) 选择任何一种Binding,设置 TransactionFlow = false(默认)。 (2) 设置 TransactionFlow(TransactionFlowOption.NotAllowed)。 (3) 设置 OperationBehavior(TransactionScopeRequired=true)。
## 三、WCF事务服务的实现
上面内容对WCF中事务进行了一个详细的介绍,下面具体通过一个实例来说明WCF中如何实现对事务的支持。首先还是按照前面博文中介绍的步骤来实现该实例。
第一步:创建WCF契约和契约的实现,具体的实现代码如下所示:
```
namespace WCFContractAndService
{
// 服务契约
[ServiceContract(SessionMode= SessionMode.Required)]
//[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = false)]
public interface IOrderService
{
// 操作契约
[OperationContract]
// 控制客户端的事务是否传播到服务
// TransactionFlow的值会包含在服务发布的元数据上
[TransactionFlow(TransactionFlowOption.NotAllowed)]
List GetCustomers();
[OperationContract]
[TransactionFlow(TransactionFlowOption.NotAllowed)]
List GetProducts();
[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
string PlaceOrder(Order order);
[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
string AdjustInventory(int productId, int quantity);
[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
string AdjustBalance(int customerId, decimal amount);
}
[DataContract]
public class Customer
{
[DataMember]
public int CustomerId { get; set; }
[DataMember]
public string CompanyName { get; set; }
[DataMember]
public decimal Balance { get; set; }
}
[DataContract]
public class Product
{
[DataMember]
public int ProductId { get; set; }
[DataMember]
public string ProductName { get; set; }
[DataMember]
public decimal Price { get; set; }
[DataMember]
public int OnHand { get; set; }
}
[DataContract]
public class Order
{
[DataMember]
public int CustomerId { get; set; }
[DataMember]
public int ProductId { get; set; }
[DataMember]
public decimal Price { get; set; }
[DataMember]
public int Quantity { get; set; }
[DataMember]
public decimal Amount { get; set; }
}
}
namespace WCFContractAndService
{
// 服务实现
[ServiceBehavior(
TransactionIsolationLevel = IsolationLevel.Serializable,
TransactionTimeout= "00:00:30",
InstanceContextMode = InstanceContextMode.PerSession,
TransactionAutoCompleteOnSessionClose = true)]
public class OrderService :IOrderService
{
private List customers = null;
private List products = null;
private int orderId = 0;
private string conString = Properties.Settings.Default.TransactionsConnectionString;
public List GetCustomers()
{
customers = new List();
using (var cnn = new SqlConnection(conString))
{
using (var cmd = new SqlCommand("SELECT * " + "FROM Customers ORDER BY CustomerId", cnn))
{
cnn.Open();
using (SqlDataReader CustomersReader = cmd.ExecuteReader())
{
while (CustomersReader.Read())
{
var customer = new Customer();
customer.CustomerId = CustomersReader.GetInt32(0);
customer.CompanyName = CustomersReader.GetString(1);
customer.Balance = CustomersReader.GetDecimal(2);
customers.Add(customer);
}
}
}
}
return customers;
}
public List GetProducts()
{
products = new List();
using (var cnn = new SqlConnection(conString))
{
using (var cmd = new SqlCommand(
"SELECT * " +
"FROM Products ORDER BY ProductId", cnn))
{
cnn.Open();
using (SqlDataReader productsReader =
cmd.ExecuteReader())
{
while (productsReader.Read())
{
var product = new Product();
product.ProductId = productsReader.GetInt32(0);
product.ProductName = productsReader.GetString(1);
product.Price = productsReader.GetDecimal(2);
product.OnHand = productsReader.GetInt16(3);
products.Add(product);
}
}
}
}
return products;
}
// 设置服务的环境事务
// 使用Client模式,即使用客户端的事务
[OperationBehavior(TransactionScopeRequired =true, TransactionAutoComplete = false)]
public string PlaceOrder(Order order)
{
using (var conn = new SqlConnection(conString))
{
var cmd = new SqlCommand(
"Insert Orders (CustomerId, ProductId, " +
"Quantity, Price, Amount) " + "Values( " +
"@customerId, @productId, @quantity, " +
"@price, @amount)", conn);
cmd.Parameters.Add(new SqlParameter(
"@customerId", order.CustomerId));
cmd.Parameters.Add(new SqlParameter(
"@productid", order.ProductId));
cmd.Parameters.Add(new SqlParameter(
"@price", order.Price));
cmd.Parameters.Add(new SqlParameter(
"@quantity", order.Quantity));
cmd.Parameters.Add(new SqlParameter(
"@amount", order.Amount));
try
{
conn.Open();
if (cmd.ExecuteNonQuery() <= 0)
{
return "The order was not placed";
}
cmd = new SqlCommand(
"Select Max(OrderId) From Orders " +
"Where CustomerId = @customerId", conn);
cmd.Parameters.Add(new SqlParameter(
"@customerId", order.CustomerId));
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
orderId = Convert.ToInt32(reader[0].ToString());
}
}
return string.Format("Order {0} was placed", orderId);
}
catch (Exception ex)
{
throw new FaultException(ex.Message);
}
}
}
// 使用Client模式,即使用客户端的事务
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
public string AdjustInventory(int productId, int quantity)
{
using (var conn = new SqlConnection(conString))
{
var cmd = new SqlCommand(
"Update Products Set OnHand = " +
"OnHand - @quantity " +
"Where ProductId = @productId", conn);
cmd.Parameters.Add(new SqlParameter(
"@quantity", quantity));
cmd.Parameters.Add(new SqlParameter(
"@productid", productId));
try
{
conn.Open();
if (cmd.ExecuteNonQuery() <= 0)
{
return "The inventory was not updated";
}
else
{
return "The inventory was updated";
}
}
catch (Exception ex)
{
throw new FaultException(ex.Message);
}
}
}
// 使用Client模式,即使用客户端的事务
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
public string AdjustBalance(int customerId, decimal amount)
{
using (var conn = new SqlConnection(conString))
{
var cmd = new SqlCommand(
"Update Customers Set Balance = " +
"Balance - @amount " +
"Where CustomerId = @customerId", conn);
cmd.Parameters.Add(new SqlParameter(
"@amount", amount));
cmd.Parameters.Add(new SqlParameter(
"@customerId", customerId));
try
{
conn.Open();
if (cmd.ExecuteNonQuery() <= 0)
{
return "The balance was not updated";
}
else
{
return "The balance was updated";
}
}
catch (Exception ex)
{
throw new FaultException(ex.Message);
}
}
}
}
}
```
上面的服务契约和服务实现与传统的实现没什么区别。这里使用IIS来宿主WCF服务。
第二步:宿主的实现。创建一个空的Web的项目,并添加WCF服务文件,具体内容如下所示:
对应的Web.config的内容如下所示:
```
```
这里采用了wsHttpBinding绑定,并设置其transactionFlow属性为true使其支持事务传播。接下来看看客户端的实现。
第三步:WCF客户端的实现,通过添加服务引用的方式来生成代理类。这里的客户端是WinForm程序。
```
1 public partial class Form1 : Form
2 {
3 public Form1()
4 {
5 InitializeComponent();
6 }
7
8 private Customer customer = null;
9 private List customers = null;
10 private Product product = null;
11 private List products = null;
12 private OrderServiceClient proxy = null;
13 private Order order = null;
14 private string result = String.Empty;
15
16 private void Form1_Load(object sender, EventArgs e)
17 {
18 proxy = new OrderServiceClient("WSHttpBinding_IOrderService");
19 GetCustomersAndProducts();
20 }
21
22 private void GetCustomersAndProducts()
23 {
24 customers = proxy.GetCustomers().ToList();
25 customerBindingSource.DataSource = customers;
26
27 products = proxy.GetProducts().ToList();
28 productBindingSource.DataSource = products;
29 }
30
31 private void placeOrderButton_Click(object sender, EventArgs e)
32 {
33 customer = (Customer)this.customerBindingSource.Current;
34 product = (Product)this.productBindingSource.Current;
35 Int32 quantity = Convert.ToInt32(quantityTextBox.Text);
36
37 order = new Order();
38 order.CustomerId = customer.CustomerId;
39 order.ProductId = product.ProductId;
40 order.Price = product.Price;
41 order.Quantity = quantity;
42 order.Amount = order.Price * Convert.ToDecimal(order.Quantity);
43
44 // 事务处理
45 using (var tranScope = new TransactionScope())
46 {
47 proxy = new OrderServiceClient("WSHttpBinding_IOrderService");
48 {
49 try
50 {
51 result = proxy.PlaceOrder(order);
52 MessageBox.Show(result);
53
54 result = proxy.AdjustInventory(product.ProductId, quantity);
55 MessageBox.Show(result);
56
57 result = proxy.AdjustBalance(customer.CustomerId,
58 Convert.ToDecimal(quantity) * order.Price);
59 MessageBox.Show(result);
60
61 proxy.Close();
62 tranScope.Complete(); // Cmmmit transaction
63 }
64 catch (FaultException faultEx)
65 {
66 MessageBox.Show(faultEx.Message +
67 "\n\nThe order was not placed");
68
69 }
70 catch (ProtocolException protocolEx)
71 {
72 MessageBox.Show(protocolEx.Message +
73 "\n\nThe order was not placed");
74 }
75 }
76 }
77
78 // 成功提交后强制刷新界面
79 quantityTextBox.Clear();
80 try
81 {
82 proxy = new OrderServiceClient("WSHttpBinding_IOrderService");
83 GetCustomersAndProducts();
84 }
85 catch (FaultException faultEx)
86 {
87 MessageBox.Show(faultEx.Message);
88 }
89 }
90 }
```
从上面代码可以看出,WCF事务的实现是利用[TransactionScope](http://msdn.microsoft.com/zh-cn/library/system.transactions.transactionscope(v=vs.110).aspx)事务类来完成的。下面让我们看看程序的运行结果。在运行程序之前,我们必须运行SQL脚本来创建程序中的使用的数据库,具体的脚本如下所示:
```
1 USE [TransactionsDemo]
2 GO
3 /****** Object: Table [dbo].[Customers] Script Date: 01/15/2009 08:14:25 ******/
4 SET ANSI_NULLS ON
5 GO
6 SET QUOTED_IDENTIFIER ON
7 GO
8 CREATE TABLE [dbo].[Customers](
9 [CustomerId] [int] IDENTITY(1,1) NOT NULL,
10 [Name] [nvarchar](20) NOT NULL,
11 [Balance] [smallmoney] NOT NULL, check(Balance >= 0),
12 CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED
13 (
14 [CustomerId] ASC
15 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
16 ) ON [PRIMARY]
17 GO
18 /****** Object: Table [dbo].[Products] Script Date: 01/15/2009 08:14:25 ******/
19 SET ANSI_NULLS ON
20 GO
21 SET QUOTED_IDENTIFIER ON
22 GO
23 CREATE TABLE [dbo].[Products](
24 [ProductId] [int] IDENTITY(1,1) NOT NULL,
25 [Name] [nvarchar](20) NOT NULL,
26 [Price] [smallmoney] NOT NULL,
27 [OnHand] [smallint] NOT NULL, check(OnHand >= 0),
28 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED
29 (
30 [ProductId] ASC
31 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
32 ) ON [PRIMARY]
33 GO
34 /****** Object: Table [dbo].[Orders] Script Date: 01/15/2009 08:14:25 ******/
35 SET ANSI_NULLS ON
36 GO
37 SET QUOTED_IDENTIFIER ON
38 GO
39 CREATE TABLE [dbo].[Orders](
40 [OrderId] [int] IDENTITY(1,1) NOT NULL,
41 [CustomerId] [int] NOT NULL,
42 [ProductId] [int] NOT NULL,
43 [Quantity] [smallint] NOT NULL,
44 [Price] [smallmoney] NOT NULL,
45 [Amount] [smallmoney] NOT NULL,
46 CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED
47 (
48 [OrderId] ASC
49 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
50 ) ON [PRIMARY]
51 GO
52 INSERT Customers (Name, Balance) VALUES ('Contoso', 10000)
53 INSERT Customers (Name, Balance) VALUES ('Northwind', 25000)
54 INSERT Customers (Name, Balance) VALUES ('Litware', 50000)
55 INSERT Products (Name, Price, OnHand) VALUES ('Wood', 100, 1000)
56 INSERT Products (Name, Price, OnHand) VALUES ('Wallboard', 200, 2500)
57 INSERT Products (Name, Price, OnHand) VALUES ('Pipe', 500, 5000)
58 GO
```
生成程序使用的数据库之后,按F5运行WCF客户端程序,并在出现的界面中购买Wood材料100,运行结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4981e10.png)
单击Place order按钮后,即执行下订单操作,如果订单成功后,将会更新产品的库存和用户的余额,你将看到如下图所示的运行结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb499051a.png)
## 四、小结
到这里,关于WCF中事务的介绍就结束了。WCF支持四种事务模式,Client/Service、Client、Service和None,对于每种模式都有其不同的配置。一般尽量使用Client/Service或Client事务模式。WCF事务的实现借助于已有的System.Transaction实现本地事务的编程,而分布式事务则借助MSDTC分布式事务协调机制来实现。WCF提供了支持事务传播的绑定协议包括:wsHttpBinding、WSDualHttpBinding、WSFederationBinding、NetTcpBinding和NetNamedPipeBinding,最后两个绑定允许选择WS-AT协议或OleTx协议,而其他绑定都使用标准的WS-AT协议。在一一篇博文将分享WCF对消息队列的支持。
本文所有源代码:[WCFTransaction.zip](http://files.cnblogs.com/zhili/WCFTransaction.zip)
';
跟我一起学WCF(9)——WCF回调操作的实现
最后更新于:2022-04-02 00:13:41
# 跟我一起学WCF(9)——WCF回调操作的实现
## 一、引言
在上一篇文章中介绍了WCF对Session的支持,在这篇文章中将详细介绍WCF支持的操作。在WCF中,除了支持经典的请求/应答模式外,还提供了对单向操作、双向回调操作模式的支持,此外还有流操作的支持。接下来将详细介绍下这几种操作,并实现一个双向回调操作的例子。
## 二、WCF操作详解
## 2.1 请求—应答操作
请求应答模式是WCF中默认的操作模式。请求应答模式指的是:客户端以消息形式发送请求,它会阻塞客户端直到收到应答消息。应答的默认超时时间为1分钟,如果超过这一时间服务仍然没有应答,客户端就会获得一个TimeOutException异常。WCF中除了NetPeerTcpBinding和NetMsmqBinding绑定,所有的绑定都支持请求—应答操作。
## 2.2 单向操作
单向操作是没有返回值的,客户端不关心调用是否成功。单向操作指的是:客户端一旦发出调用请求,WCF会生成一个请求消息发送给服务端,但客户端并不需要接收相关的应答消息。因此,单向操作不能有返回值,并且服务端抛出的任何异常都不会传递给客户端。所以客户端如果需要捕获服务端发生的异常,此时不能把操作契约的IsOneWay属性设置为true,该属性的默认值为false。异常处理参考:[如何在WCF进行Exception Handling](http://www.cnblogs.com/artech/archive/2007/06/15/784090.html)。单向操作不等同于异步操作,单向操作只是在发出调用的瞬间阻塞客户端,但如果发出多个单向调用,WCF会将请求调用放入服务端的队列中,并在某个时间进行执行。队列的存储个数有限,一旦发出的调用个数超出了队列容量,则会发生阻塞现象,此时调用请求无法放入丁烈,直到有其他请求被处理,即队列中的请求出队列后,产生阻塞的调用就会放入队列,并解除对客户端的阻塞。WCF中所有绑定都支持单向操作。WCF中实现单向操作只需要设置IsOneWay属性为true即可。这里需要注意一点:由于单向操作没有应答消息,因此它不能包含返回结果。
## 2.3 回调操作
WCF支持服务将调用返回给它的客户端。在回调期间,服务成为了客户端,而客户端成为了服务。在WCF中,并不是所有的绑定都支持回调操作,只有具有双向能力的绑定才能够用于回调。例如,HTTP本质上是与连接无关的,所以它不能用于回调,因此我们不能基于basicHttpBinding和wsHttpBinding绑定使用回调,WCF为NetTcpBinding和NetNamedPipeBinding提供了对回调的支持,因为TCP和IPC协议都支持双向通信。为了让Http支持回调,WCF提供了WsDualHttpBinding绑定,它实际上设置了两个Http通道:一个用于从客户端到服务的调用,另一个用于服务到客户端的调用。
回调操作时通过回调契约来实现的,回调契约属于服务契约的一部分,一个服务契约最多只能包含一个回调契约。一旦定义了回调契约,就需要客户端实现回调契约。在WCF中,可以通过ServiceContract的[CallbackContract](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.servicecontractattribute.callbackcontract(v=vs.110).aspx)属性来定义回调契约。具体的实现代码如下所示:
```
// 指定回调契约为ICallback
[ServiceContract(Namespace="http://cnblog.com/zhili/", CallbackContract=typeof(ICallback))]
public interface ICalculator
{
[OperationContract(IsOneWay = true)]
void Multiple(double a, double b);
}
// 回调契约的定义,此时回调契约不需要应用ServiceContractAttribute特性
public interface ICallback
{
[OperationContract(IsOneWay = true)]
void DisplayResult(double x, double y, double result);
}
```
在上面代码中,回调契约不必标记ServiceContract特性,因为类型只要被定义为回调契约,就代表它具有ServiceContract特性,但仍然需要为所有的回调接口中的方法标记OperationContract特性。
## 2.4 流操作
在默认情况下,当客户端与服务交换消息时,这些消息会被放入到接收端的缓存中,一旦接收到完整的消息,就立即被传递处理。无论是客户端发送消息到服务还是服务返回消息给客户端,都是如此。当客户端调用服务时,只要接收到完整的消息,服务就会被调用,当包含了调用结果的返回消息被客户端完整接收时,才会接触对客户端的阻塞。对于数据量小的消息,这种交换模式提供了简单的编程模型,因为接收消息的耗时较短,然而,一旦处理数据量更大的消息,例如包含了多媒体内容或大文件,如果每次都要等到完整地接收消息之后才能解除阻塞,这未免也不现实。为了解决这样的问题,WCF允许接收端通过通道接收消息的同时,启动对消息数据的处理,这样的处理过程称为流传输模型。对于具有大量负载的消息而言,流操作改善了系统的吞吐量和响应速度,因为在发生和接收消息的同时,不管是发送端还是接收端都不会被阻塞。
## 三、WCF中回调操作的实现
上面介绍了WCF中支持的四种操作,下面就具体看看WCF中回调操作的实现。该例子的基本原理是:客户端调用服务操作,服务操作通过客户端上下文实例调用客户端操作。下面还是按照三个步骤来实现该WCF程序。
第一步:同样是实现WCF服务契约和契约的实现。具体的实现代码如下所示:
```
1 // 指定回调契约为ICallback
2 [ServiceContract(Namespace="http://cnblog.com/zhili/", CallbackContract=typeof(ICallback))]
3 public interface ICalculator
4 {
5 [OperationContract(IsOneWay = true)]
6 void Multiple(double a, double b);
7 }
8
9 // 回调契约的定义,此时回调契约不需要应用ServiceContractAttribute特性
10 public interface ICallback
11 {
12 [OperationContract(IsOneWay = true)]
13 void DisplayResult(double x, double y, double result);
14 }
15
16 // 服务契约的实现
17 public class CalculatorService : ICalculator
18 {
19 #region ICalculator Members
20 public void Multiple(double a, double b)
21 {
22 double result = a * b;
23 // 通过客户端实例通道
24 ICallback callback = OperationContext.Current.GetCallbackChannel();
25
26 // 对客户端操作进行回调
27 callback.DisplayResult(a, b, result);
28 }
29 #endregion
30 }
```
第二步:实现服务宿主。这里还是以控制台程序作为服务宿主。具体的实现代码如下所示:
```
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
6 {
7 host.Opened += delegate
8 {
9 Console.WriteLine("Service start now....");
10 };
11
12 host.Open();
13 Console.Read();
14 }
15 }
16 }
```
宿主对应的配置文件内容如下所示:
```
```
第三步:实现客户端。由于服务端来对客户端操作进行回调,所以此时客户端需要实现回调契约。首先以管理员权限启动服务宿主,服务宿主启动成功之后,客户端通过添加服务引用的方式来生成客户端代理类,此时需要在添加服务引用窗口地址中输入:http://localhost:8080/Metadata。添加服务引用成功之后,接着在客户端实现回调契约,具体的实现代码如下所示:
```
1 // 客户端中对回调契约的实现
2 public class CallbackWCFService : ICalculatorCallback
3 {
4 public void DisplayResult(double a, double b, double result)
5 {
6 Console.WriteLine("{0} * {1} = {2}", a, b, result);
7 }
8 }
```
接下来就是实现测试回调操作的客户端代码了。具体的实现步骤是:实例化一个回调类的实例,然后把它作为上下文实例的操作,最后把上下文实例作为客户端代理的参数来实例化客户端代理。具体的实现代码如下所示:
```
1 // 客户端实现,测试回调操作
2 class Program
3 {
4 static void Main(string[] args)
5 {
6 InstanceContext instanceContex = new InstanceContext(new CallbackWCFService());
7 CalculatorClient proxy = new CalculatorClient(instanceContex);
8 proxy.Multiple(2,3);
9
10 Console.Read();
11 }
12 }
```
下面运行运行该程序来检测下该程序是否能够成功回调,首先以管理员权限启动服务宿主程序,再启动客户端程序,如果回调成功,你将看到如下图所示的运行结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb495008a.png)
这里只是演示了回调操作的实现,关于流操作的实现,这里就不再去实现了,等具体需要的时候再去研究吧,同时给出关于流操作实现的参考文章:
[Stream Operation in WCF](http://www.codeproject.com/Articles/36973/Stream-Operation-in-WCF)
[WCF流处理(Streaming)机制](http://blog.csdn.net/frankxulei/article/details/4735907)
## 四、总结
到这里,WCF操作的内容就分享结束了,本文首先介绍了在WCF中支持四种操作:请求-应答操作、单向操作、回调操作和流操作,WCF中默认的操作时请求-应答操作,最后实现了一个回调操作的实例。
本文所有源码:[WCFCallbackOperation.zip](http://files.cnblogs.com/zhili/WCFCallbackOperation.zip)
';
跟我一起学WCF(8)——WCF中Session、实例管理详解
最后更新于:2022-04-02 00:13:38
# 跟我一起学WCF(8)——WCF中Session、实例管理详解
## 一、引言
由前面几篇博文我们知道,WCF是微软基于SOA建立的一套在分布式环境中各个相对独立的应用进行交流(Communication)的框架,它实现了最新的基于WS-*规范。按照SOA的原则,相对独自的业务逻辑以Service的形式进行封装,调用者通过消息(Messaging)的方式来调用服务。对于承载某个业务功能实现的服务应该具有上下文(Context)无关性,意思就是说构造服务的操作(Operation)不应该绑定到具体的调用上下文,对于任何的调用,具有什么的样输入就会对应怎样的输出。因为SOA一个最大的目标是尽可能地实现重用,只有具有Context无关性,服务才能最大限度的重用。即从软件架构角度理解为,一个模块只有尽可能的独立,即具有上下文无关性,才能被最大限度的重用。软件体系一直在强调低耦合也是这个道理。
但是在某些场景下,我们却希望系统为我们创建一个Session来保留Client和Service的交互的状态,如Asp.net中Session的机制一样,WCF也提供了对Session的支持。下面就具体看看WCF中对Session的实现。
## 二、WCF中Session详细介绍
## 2.1 Asp.net的Session与WCF中的Session
在WCF中,Session属于Service Contract的范畴,并在Service Contract定义中通过[SessionModel](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.servicecontractattribute.sessionmode(v=vs.90).aspx)参数来实现。WCF中会话具有以下几个重要的特征:
* Session都是由Client端显示启动和终止的。
在WCF中Client通过创建的代理对象来和服务进行交互,在支持Session的默认情况下,Session是和具体的代理对象绑定在一起,当Client通过调用代理对象的某个方法来访问服务时,Session就被初始化,直到代理的关闭,Session则被终止。我们可以通过两种方式来关闭Proxy:一是调用[ICommunicationObject.Close 方法](http://msdn.microsoft.com/zh-cn/library/ms195520(v=vs.110).aspx),二是调用[ClientBase<TChannel>.Close 方法](http://msdn.microsoft.com/zh-cn/library/ms575273(v=vs.110).aspx) 。我们也可以通过服务中的某个操作方法来初始化、或者终止Session,可以通过OperationContractAttribute的IsInitiating和[IsTerminating](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.operationcontractattribute.isterminating(v=vs.110).aspx)参数来指定初始化和终止Session的Operation。
* 在WCF会话期间,传递的消息按照它发送的顺序被接收。
* WCF并没有为Session的支持保存相关的状态数据。
讲到Session,做过Asp.net开发的人,自然想到的就是Asp.net中的Session。它们只是名字一样,在实现机制上有很大的不同。Asp.net中的Session具有以下特性:
* Asp.net的Session总是由服务端启动的,即在服务端进行初始化的。
* Asp.net中的Session是无需,不能保证请求处理是有序的。
* Asp.net是通过在服务端以某种方式保存State数据来实现对Session的支持,例如保存在Web Server端的内存中。
## 2.2 WCF中服务实例管理
对于Client来说,它实际上不能和Service进行直接交互,它只能通过客户端创建的Proxy来间接地和Service进行交互,然而真正的调用而是通过服务实例来进行的。我们把通过Client的调用来创建最终的服务实例过程称作激活,在.NET Remoting中包括Singleton模式、SingleCall模式和客户端激活方式,WCF中也有类似的服务激活方式:单调服务(PerCall)、会话服务(PerSession)和单例服务(Singleton)。
* **单调服务(Percall)**:为每个客户端请求分配一个新的服务实例。类似.NET Remoting中的SingleCall模式
* **会话服务(Persession)**:在会话期间,为每次客户端请求共享一个服务实例,类似.NET Remoting中的客户端激活模式。
* **单例服务(Singleton)**:所有客户端请求都共享一个相同的服务实例,类似于.NET Remoting的Singleton模式。但它的激活方式需要注意一点:当为对于的服务类型进行Host的时候,与之对应的服务实例就被创建出来,之后所有的服务调用都由这个服务实例进行处理。
WCF中服务激活的默认方式是PerSession,但不是所有的Bingding都支持Session,比如BasicHttpBinding就不支持Session。你也可以通过下面的方式使ServiceContract不支持Session.
```
[ServiceContract(SessionMode = SessionMode.NotAllowed)]
```
下面分别介绍下这三种激活方式的实现。
## 三、WCF中实例管理的实现
WCF中服务激活的默认是PerSession的方式,下面就看看PerSession的实现方式。我们还是按照前面几篇博文的方式来实现使用PerSession方式的WCF服务程序。
第一步:自然是实现我们的WCF契约和契约的服务实现。具体的实现代码如下所示:
```
1 // 服务契约的定义
2 [ServiceContract]
3 public interface ICalculator
4 {
5 [OperationContract(IsOneWay = true)]
6 void Increase();
7
8 [OperationContract]
9 int GetResult();
10 }
11
12 // 契约的实现
13 public class CalculatorService : ICalculator, IDisposable
14 {
15 private int _nCount = 0;
16
17 public CalculatorService()
18 {
19 Console.WriteLine("CalulatorService object has been created");
20 }
21
22 // 为了看出服务实例的释放情况
23 public void Dispose()
24 {
25 Console.WriteLine("CalulatorService object has been Disposed");
26 }
27
28 #region ICalulator Members
29 public void Increase()
30 {
31 // 输出Session ID
32 Console.WriteLine("The Add method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
33 this._nCount++;
34 }
35
36 public int GetResult()
37 {
38 Console.WriteLine("The GetResult method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
39 return this._nCount;
40 }
41 #endregion
42 }
```
为了让大家对服务对象的创建和释放有一个直观的认识,我特意对服务类实现了构造函数和IDisposable接口,同时在每个操作中输出当前的Session ID。
第二步:实现服务宿主程序。这里还是采用控制台程序作为服务宿主程序,具体的实现代码如下所示:
```
1 // 服务宿主程序
2 class Program
3 {
4 static void Main(string[] args)
5 {
6 using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
7 {
8 host.Opened += delegate
9 {
10 Console.WriteLine("The Calculator Service has been started, begun to listen request...");
11 };
12
13 host.Open();
14 Console.ReadLine();
15 }
16 }
17 }
```
对应的配置文件为:
```
```
第三步:实现完了服务宿主程序,接下来自然是实现客户端程序来访问服务操作。这里的客户端也是控制台程序,具体的实现代码如下所示:
```
1 // 客户端程序实现
2 class Program
3 {
4 static void Main(string[] args)
5 {
6 // Use ChannelFactory to create WCF Service proxy
7 ChannelFactory calculatorChannelFactory = new ChannelFactory("HttpEndPoint");
8 Console.WriteLine("Create a calculator proxy :proxy1");
9 ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
10 Console.WriteLine("Invoke proxy1.Increate() method");
11 proxy1.Increase();
12 Console.WriteLine("Invoke proxy1.Increate() method again");
13 proxy1.Increase();
14 Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult());
15
16 Console.WriteLine("Create another calculator proxy: proxy2");
17 ICalculator proxy2 = calculatorChannelFactory.CreateChannel();
18 Console.WriteLine("Invoke proxy2.Increate() method");
19 proxy2.Increase();
20 Console.WriteLine("Invoke proxy2.Increate() method again");
21 proxy2.Increase();
22 Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult());
23
24 Console.ReadLine();
25 }
26 }
```
客户端对应的配置文件内容如下所示:
```
```
经过上面三步,我们就完成了PerSession方式的WCF程序了,下面看看该程序的运行结果。
首先,以管理员权限运行服务寄宿程序,运行成功后,你将看到如下图所示的画面:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb48b3006.png)
接下来,运行客户端对服务操作进行调用,运行成功后,你将看到服务宿主的输出和客户端的输出情况如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb48c191d.png)
从客户端的运行结果可以看出,虽然我们两次调用了Increase方法来增加_nCount的值,但是最终的运行结果仍然是0。这样的运行结果好像与我们之前所说的WCF默认Session支持矛盾,因为如果WCF默认的方式PerSession的话,则服务实例是和Proxy绑定在一起,当Proxy调用任何一个操作的时候Session开始,从此Session将会与Proxy具有一样的生命周期。按照这个描述,客户端运行的结果应该是2而不是0。这里,我只能说运行结果并没有错,因为有图有真相嘛,那到底是什么原因导致客户端获得_nCount值是0呢?其实在前面已经讲到过,并不是所有的绑定都是支持Session的,上面程序的实现我们使用的basicHttpBinding,而basicHttpBinding是不支持Session方式的,所以WCF会采用PerCall的方式创建Service Instance,所以在服务端中对于每一个Proxy都有3个对象被创建,两个是对Increase方法的调用会导致服务实例的激活,另一个是对GetResult方法的调用导致服务实例的激活。因为是PerCall方式,所以每次调用完之后,就会对服务实例进行释放,所以对应的就有3行服务对象释放输出。并且由于使用的是不支持Session的binding,所以Session ID的输出也为null。所以,上面WCF程序其实是PerCall方式的实现。
既然,上面的运行结果是由于使用了不支持Session的basicHttpBinding导致的,下面看看使用一个支持Session的Binding:wsHttpBinding来看看运行结果是怎样的,这里的修改很简单,只需要把宿主和客户端的配置文件把绑定类型修改为wsHttpBinding就可以了。
```
```
现在我们再运行下上面的程序来看看此时的执行结果,具体的运行结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb48d9d9e.png)
从上面的运行结果可以看出,此时两个Proxy的运行结果都是2,可以看出此时服务激活方式采用的是PerSession方式。此时对于服务端就只有两个服务实例被创建了,并且对于每个服务实例具有相同的Session ID。 另外由于Client的Proxy还依然存在,服务实例也不会被回收掉,从上面服务端运行的结果也可以证实这点,因为运行结果中没有对象呗Disposable的输出。你可以在客户端显式调用ICommunicationObject.Close方法来显式关闭掉Proxy,在客户端添加对Proxy的显示关闭代码,此时客户端的代码修改为如下所示:
```
1 // 客户端程序实现
2 class Program
3 {
4 static void Main(string[] args)
5 {
6 // Use ChannelFactory to create WCF Service proxy
7 ChannelFactory calculatorChannelFactory = new ChannelFactory("HttpEndPoint");
8 Console.WriteLine("Create a calculator proxy :proxy1");
9 ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
10 Console.WriteLine("Invoke proxy1.Increate() method");
11 proxy1.Increase();
12 Console.WriteLine("Invoke proxy1.Increate() method again");
13 proxy1.Increase();
14 Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult());
15 **(proxy1 as ICommunicationObject).Close(); // 显示关闭Proxy**
16
17 Console.WriteLine("Create another calculator proxy: proxy2");
18 ICalculator proxy2 = calculatorChannelFactory.CreateChannel();
19 Console.WriteLine("Invoke proxy2.Increate() method");
20 proxy2.Increase();
21 Console.WriteLine("Invoke proxy2.Increate() method again");
22 proxy2.Increase();
23 Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult());
24 **(proxy2 as ICommunicationObject).Close();** 25
26 Console.ReadLine();
27 }
28 }
```
此时,服务对象的Dispose()方法将会调用,此时服务端的运行结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb48edc84.png)
上面演示了默认支持Session的情况,下面我们修改服务契约使之不支持Session,此时只需要知道ServiceContract的SessionMode为NotAllowed即可。
```
[ServiceContract**(SessionMode= SessionMode.NotAllowed)**] // 是服务契约不支持Session
public interface ICalculator
{
[OperationContract(IsOneWay = true)]
void Increase();
[OperationContract]
int GetResult();
}
```
此时,由于服务契约不支持Session,此时服务激活方式采用的仍然是PerCall。运行结果与前面采用不支持Session的绑定的运行结果一样,这里就不一一贴图了。
除了通过显式修改ServiceContract的SessionMode来使服务契约支持或不支持Session外,还可以定制操作对Session的支持。定制操作对Session的支持可以通过OperationContract的IsInitiating和InTerminating属性设置。
```
1 // 服务契约的定义
2 [ServiceContract(SessionMode= SessionMode.Required)] // 显式使服务契约支持Session
3 public interface ICalculator
4 {
5 // IsInitiating:该值指示方法是否实现可在服务器上启动会话(如果存在会话)的操作,默认值是true
6 // IsTerminating:获取或设置一个值,该值指示服务操作在发送答复消息(如果存在)后,是否会导致服务器关闭会话,默认值是false
7 [OperationContract(IsOneWay = true, IsInitiating =true, IsTerminating=false )]
8 void Increase();
9
10 [OperationContract(IsInitiating = true, IsTerminating = true)]
11 int GetResult();
12 }
```
在上面代码中,对两个操作都设置InInitiating的属性为true,意味着调用这两个操作都会启动会话,而把GetResult操作的IsTerminating设置为true,意味着调用完这个操作后,会导致服务关闭掉会话,因为在Session方式下,Proxy与Session有一致的生命周期,所以关闭Session也就是关闭proxy对象,所以如果后面再对proxy对象的任何一个方法进行调用将会导致异常,下面代码即演示了这种情况。
```
1 // 客户端程序实现
2 class Program
3 {
4 static void Main(string[] args)
5 {
6 // Use ChannelFactory to create WCF Service proxy
7 ChannelFactory calculatorChannelFactory = new ChannelFactory("HttpEndPoint");
8 Console.WriteLine("Create a calculator proxy :proxy1");
9 ICalculator proxy1 = calculatorChannelFactory.CreateChannel();
10 Console.WriteLine("Invoke proxy1.Increate() method");
11 proxy1.Increase();
12 Console.WriteLine("Invoke proxy1.Increate() method again");
13 proxy1.Increase();
14 Console.WriteLine("The result return via proxy1.GetResult() is: {0}", proxy1.GetResult());
15 **try
16 {
17 proxy1.Increase(); // session关闭后对proxy1.Increase方法调用将会导致异常
18 }
19 catch (Exception ex) // 异常捕获
20 {
21 Console.WriteLine("在Session关闭后调用Increase方法失败,错误信息为:{0}", ex.Message);
22 }** 23
24 Console.WriteLine("Create another calculator proxy: proxy2");
25 ICalculator proxy2 = calculatorChannelFactory.CreateChannel();
26 Console.WriteLine("Invoke proxy2.Increate() method");
27 proxy2.Increase();
28 Console.WriteLine("Invoke proxy2.Increate() method again");
29 proxy2.Increase();
30 Console.WriteLine("The result return via proxy2.GetResult() is: {0}", proxy2.GetResult());
31
32 Console.ReadLine();
33 }
34 }
```
此时运行结果也验证我们上面的分析,客户端和服务端的运行结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb490d13e.png)
上面演示了PerSession和PerCall的两种服务对象激活方式,下面看看Single的激活方式运行的结果。首先通过ServiceBehavior的InstanceContextMode属性显式指定激活方式为Single,由于[ServiceBehaviorAttribute](http://msdn.microsoft.com/zh-cn/library/System.ServiceModel.ServiceBehaviorAttribute(v=vs.110).aspx)特性只能应用于类上,所以把该特性应用于CalculatorService类上,此时服务实现的代码如下所示:
```
// 契约的实现
// ServiceBehavior属性只能应用在类上
**[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] // 显示指定PerSingle方式**
public class CalculatorService : ICalculator, IDisposable
{
private int _nCount = 0;
public CalculatorService()
{
Console.WriteLine("CalulatorService object has been created");
}
// 为了看出服务实例的释放情况
public void Dispose()
{
Console.WriteLine("CalulatorService object has been Disposed");
}
#region ICalulator Members
public void Increase()
{
// 输出Session ID
Console.WriteLine("The Add method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
this._nCount++;
}
public int GetResult()
{
Console.WriteLine("The GetResult method is invoked and the current session ID is: {0}", OperationContext.Current.SessionId);
return this._nCount;
}
#endregion
}
```
此时运行服务宿主的输出结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb49265d7.png)
从运行结果可以看出,对于Single方式,服务实例在服务类型被寄宿的时候就已经创建了,对于PerCall和PerSession方式而是在通过Proxy调用相应的服务操作之后,服务实例才开始创建的。下面运行客户端程序,你将看到如下图所示的运行结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb493b301.png)
此时,第二个Proxy返回的结果是4而不是2,这是因为采用Single方式只存在一个服务实例,所有的调用状态都将保留,所以_nCount的值在原来的基础上继续累加。
## 四、总结
到这里,本文的分享就结束了,本文主要分享了WCF中实例管理的实现。从WCF的实例实现可以看出,WCF实例实现是借鉴了.NET Remoting中实例实现,然后分别分享了服务实例三种激活方式在WCF中的实现,并通过对运行结果进行对比来让大家理解它们之间的区别。
本文所以源码:[WCFInstanceManager.zip](http://files.cnblogs.com/zhili/WCFInstanceManager.zip)
';
跟我一起学WCF(7)——WCF数据契约与序列化详解
最后更新于:2022-04-02 00:13:36
# 跟我一起学WCF(7)——WCF数据契约与序列化详解
## 一、引言
在前面博文介绍到,WCF的契约包括操作契约、数据契约、消息契约和错误契约,前面一篇博文已经结束了操作契约的介绍,接下来自然就是介绍数据契约了。所以本文要分享的内容就是数据契约。
## 二、数据契约的介绍
在WCF中,服务契约定义了可供调用的服务操作方法,而数据契约则是定义了服务端和客户端之间传送的自定义类型,在WCF项目中,必不可少地是传递数据,把客户端需要传递的数据传送到服务中,服务接收到数据再对其进行处理。然而在WCF中,传递的类型必须标记为[DataContractAttribute](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.datacontractattribute(v=vs.110).aspx)属性,且只有标记了DataMemberAttribute属性的属性才会被传送。下面代码是一个数据契约使用的示例:
```
1 [DataContract] // 数据契约属性声明
2 public class User
3 {
4 [DataMember(Name = "UserName")]//定义别名
5 public string Name
6 { get; set; }
7 [DataMember]
8 public string Password { get; set; }
9 [DataMember]
10 public string Email { get; set; }
11
12 // 没有[DataMember]声明将不会序列化传送
13 public string Mobile { get; set; }
14
15 public string Test { get; set; }
16 }
```
上面代码在类User上使用了DataContract属性声明,则表明User类是可被WCF序列化程序可识别,并且可被序列化的。但是不是User所有数据成员都可以被需要列,只有声明了DataMemberAttribute的属性才可以被序列化。因此,在上面代码中,不会传输Mobile和Test的任何信息。同时也可以为声明为DataMember的成员定义客户端可见的别名,如DataMember(Name= "UserName"),这样在生成客户端代码时,User类定义的就是UserName属性,而不是在服务中定义的Name属性了。
## 三、序列化的详细介绍
WCF的实现原理沿用了[.NET Remoting](http://www.cnblogs.com/zhili/p/NETRemoting.html)的实现机制,客户端在调用服务公开的服务方法,这个过程必然涉及到数据的传输过程,包括客户端传输相关需要处理的数据给服务或服务传输相关处理后的结果数据给客户端。在数据传输的过程中,自然就需要进行序列化的操作,通过序列化把.NET Object序列化成可保存或传输的形式,然后通过网络协议在网络上进行传递。对于序列化的实现是由序列化器(Serializer)来负责完成的,序列化的实现原理可以理解为通过反射机制分析程序集中对应的类型,然后把对应的类型映射为一个XML的结构。
序列化在.NET Framework相关专题就有所介绍,所以它并不是一个新的概念,相关内容可以参考MSDN:[序列化](http://msdn.microsoft.com/zh-cn/library/7ay27kt9(v=vs.100).aspx)。然而.NET本身的序列化机制在WCF程序中并不适应,所以WCF又提出了新的序列化器。下面分别介绍下.NET 序列化机制和WCF中序列化机制。
## 3.1 .NET序列化机制
在.NET Framework 3.0之前,提供了3中序列化器,序列化器理解为把可序列化的类型序列化成XML的类。这三种序列化器分别是[BinaryFormatter](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.formatters.binary.binaryformatter(v=vs.110).aspx)、[SoapFormatter](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.formatters.soap.soapformatter(v=vs.110).aspx)和[XmlSerializer](http://msdn.microsoft.com/zh-cn/library/system.xml.serialization.xmlserializer(v=vs.110).aspx)类。下面分别介绍下这3种序列化器。
* BinaryFormatter类:把.NET Object序列化成二进制格式。在这个过程,对象的**公共字段和私有字段**以及类名称(包括类的程序集名),将转换成成字节流。
* SoapFormatter类:把.NET Object序列化成SOAP格式,SOAP是一种轻量、简单的,基于XML的协议。只序列化字段,包括**公共字段和私有字段**。
* XmlSerializer类:该类仅仅序列化**公共字段和属性,**且不保存类型的保真度。
对于这三种序列化机制,BinaryFormatter二进制序列化的优点是:性能高,但是不能跨平台。而SoapFormatter,XmlSerializer的优点是:跨平台、互操作性好,并且可读性强,但是传输性能不及BinaryFormatter。
在.NET原有的序列化机制中,BinaryFormatter和SoapFormatter除了要序列化对象的状态信息外,还会将程序集和版本信息持久化到流中,因为只有这样才能保证对象呗反序列为正确的对象类型副本,这就要求客户端必须拥有原有的.NET 程序集,不能满足跨平台的需求。所以WCF不得不定义自己的序列化机制来满足面向服务的需求。
## 3.2 WCF中序列化机制
在WCF中,提供了专门用来序列化和反序列操作的类,该类就是[DataContractSerializer](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.datacontractserializer(v=vs.110).aspx)类。一般而言,WCF会自动选择使用DataContractSerializer来对可序列话数据契约进行序列化,不需要开发者直接调用。WCF除了支持DataContractSerializer类来进行序列化外,还支持另外两种序列化器,这两种序列化器分别为:XMLSerializer(定义在System.XML.Serialization namespace)和[NetDataContractSerializer](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.netdatacontractserializer(v=vs.110).aspx) (定义在System.XML.Serialization namespace)。XmlSerializer类不是WCF专用的类,Asp.net Web服务统一使用该类作为序列化器,但XmlSerializer类支持的类少于DataContractSerializer列支持的类型,但它允许对生成的XML进行更多的控制,并且支持更多的XML架构定义语言(XSD)标准。它不需要在可序列化类上有任何声明性的属性。
DataContractSerializer class to serialize data types.">默认情况下,WCF 使用 [DataContractSerializer](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.datacontractserializer(v=vs.110).aspx) 类来序列化数据类型。 此序列化程序支持下列类型:
* XmlElement and DateTime, which are treated as primitives.">基元类型(如:整数、字符串和字节数组)以及某些特殊类型(如 [XmlElement](http://msdn.microsoft.com/zh-cn/library/system.xml.xmlelement(v=vs.110).aspx) 和 [DateTime](http://msdn.microsoft.com/zh-cn/library/system.datetime(v=vs.110).aspx))。
* DataContractAttribute attribute).">数据协定类型(用 [DataContractAttribute](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.datacontractattribute(v=vs.110).aspx) 属性标记的类型)。
* SerializableAttribute attribute, which include types that implement the ISerializable interface.">用 [SerializableAttribute](http://msdn.microsoft.com/zh-cn/library/system.serializableattribute(v=vs.110).aspx) 属性标记的类型,包括实现 [ISerializable](http://msdn.microsoft.com/zh-cn/library/system.runtime.serialization.iserializable(v=vs.110).aspx) 接口的类型。
* IXmlSerializable interface.">实现 [IXmlSerializable](http://msdn.microsoft.com/zh-cn/library/system.xml.serialization.ixmlserializable(v=vs.110).aspx) 接口的类型。
* 许多常见集合类型,包括许多泛型集合类型。
DataContractSerializer类与NetDataContractSerializer类类似,它们之间主要的区别在于:在使用NetDataContractSerializer进行序列化时,不需要指定序列化的类型,如:
```
NetDataContractSerializer serializer =
new NetDataContractSerializer(); // 不需要明确指定序列化的类型
serializer.WriteObject(writer, p);
// 而使用DataContractSerializer需要明确指定序列化的类型
DataContractSerializer serializer =
new DataContractSerializer(**typeof(Order)**); // 需要明确指定序列化的类型
serializer.WriteObject(writer, p);
```
## 四、WCF数据契约使用例子
介绍了那么多关于数据契约和序列化内容的介绍,下面看看数据契约具体使用的例子。
要使用数据契约,自然第一步是定义数据契约,具体数据契约的定义如下所示:
```
namespace BusinessEntity
{
[DataContract]// 数据契约属性声明
public class User
{
[DataMember(Name = "UserName")]//定义别名
public string Name
{ get; set; }
[DataMember]
public string Password { get; set; }
[DataMember]
public string Email { get; set; }
// 没有[DataMember]声明将不会序列化传送
public string Mobile { get; set; }
public string Test { get; set; }
}
}
```
第二步:定义完数据契约后,接下来就要定义我们的服务契约和服务契约的实现了。具体的实现代码如下所示:
```
// 服务契约
[ServiceContract]
//[ServiceKnownType(typeof(Order))] // 这是为了演示WCF已知类型
public interface IUserValidationService
{
[OperationContract]
bool AddNewUser(User user);
[OperationContract]
User GetUserByName(string name);
// 为了演示已知类型的操作方法
//[OperationContract]
//[ServiceKnownType(typeof(Order))]
//bool AddOrder(OrderBase order);
}
// 服务契约的实现
public class UserValidationService : IUserValidationService
{
public bool AddNewUser(User user)
{
return true;
}
public User GetUserByName(string name)
{
User user = new User { Name = name, Password = "123", Email = "123456@qq.com", Mobile = "13912331245" };
return user;
}
// 演示已知类型的操作方法
//public bool AddOrder(OrderBase order)
//{
// return true;
//}
}
```
对应的配置文件代码为:
```
```
第三步:定义完服务之后,接下来就需要实现我们的客户端来访问服务方法了。首先,通过添加服务引用的方式来生成服务客户端代理类,生成的代理类中,User的定义如下代码所示:
```
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="User", Namespace="http://schemas.datacontract.org/2004/07/BusinessEntity")]
[System.SerializableAttribute()]
public partial class User : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged
{
....
}
```
从上面代码标红的部分可以看出,服务中定义的User只应用了DataContractAttribute属性,但生成的客户端User类中多了一个SerializableAtttribute。对于SerializableAttribute属性的作用与DataContract的作用是一样的,都是标记为该类支持序列化。因为在默认情况下,用户定义的类型并不支持序列化,只有应用了SerializableAttribute或DataContractAttribute属性的,.NET序列化器才能对该类型进行序列化。然而这两者又存在不同, Serializable要求它的所有程序都要支持序列化,如果发现不支持序列化的成员就会抛出异常,即Serializable会把类型的所有成员都进行序列化,如果想某个成员不序列化化,则必须显式标记NoSerialized属性;而DataContract却不同,标记了DataContract属性的类只有标记了DataMember的成员才会被序列化,如果想类型的成员能够序列化,则应该应用DataMember属性。如果某个类型同时应用了DataContract和Serialized属性,如上面代码的User类,此时该类型将会只应用DataContract,即Serialized属性会忽略。我刚开始的疑问是,User类应用这两个属性,因为这两个属性对序列化成员有所区别,当时就纳闷到底是采取那个属性进行序列化的呢?经过查阅资料才发现了上面的结论,更多信息参考:[Serialization in Windows Communication Foundation](http://msdn.microsoft.com/zh-cn/magazine/cc163569(en-us).aspx)。
然后利用该代理类实现对服务操作的调用,具体的实现代码如下所示:
```
namespace Client
{
class Program
{
static void Main(string[] args)
{
UserValidationServiceClient wcfServiceProxy = new UserValidationServiceClient();
User newUser = new User() { UserName = "LearningHard", Email = "123456@qq.com", Password = "123" };
wcfServiceProxy.AddNewUser(newUser);
// 演示已知类型的问题
//Order order = new Order() { ID = Guid.NewGuid(), Date = DateTime.Now, Customer = "customer1", ShipAddress = "Shanghai", TotalPrice = 20.00 };
//wcfServiceProxy.AddOrder(order);
// 获得用户信息
string name = "Learning Hard Client";
User user = wcfServiceProxy.GetUserByName(name);
if (user != null)
{
Console.WriteLine("User Name is: " + user.UserName);
Console.WriteLine("Email is: " + user.Email);
}
Console.WriteLine("Press any key to continue...");
Console.Read();
}
}
}
```
经过上面的三步之后,我们就完成了WCF数据契约的实现。对于服务契约的调用过程是:客户端把相关需要序列化的对象序列化成XML格式,这里的格式与绑定的协议有关,因为上面设置的传输协议为http,所以这里应该序列化成XML格式的数据,然后再通过Http协议进行网络传递到服务,服务程序接收到传输过来的XML格式的数据,则利用DataContractSerializer反序列成User对象作为参数传递给AddNewUser方法;接着服务再把处理的后结果序列化成XML格式数据传递到客户端,客户端接收到服务程序响应的消息再进行反序列成具体的对象类型。对于操作GetUserByName的调用也是类似的。具体的运行结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb488d9ed.png)
## 五、已知类型(KnownType)
因为WCF中使用DataContractSerializer进行序列化和反序列化的,由于DataContractSerializer进行序列化和反序列化时,都必须事先确定对象的类型。如果被序列化对象或反序列化生成的对象包含不可知的类型,序列化或反序列化将失败。所以为了保证DataContractSerializer正常的序列化和反序列化,需要将“未知”类型加入DataContractSerializer“已知”类型列表中。例如下面的服务契约:
```
// 服务契约
[ServiceContract]
//[ServiceKnownType(typeof(Order))] // 这是为了演示WCF已知类型
public interface IUserValidationService
{
// 为了演示已知类型的操作方法
[OperationContract]
[ServiceKnownType(typeof(Order))]
bool AddOrder(OrderBase order);
}
```
假如,客户端同时定义了一个Order类:
以下代码能够成功通过编译,但在运行时却会失败:
原因在于我们并没有实际传递对象的引用,而是传递的是对象的XML结构。在上面的例子中,当我们传递的是Order对象而不是OrderBase对象时,服务并不知道它应该反序列为Order对象。
对于上面问题的解决办法就是让DataContractSerializer能够识别Order类型,成为DataContractSerializer的已知类型(Known Type)。DataContractSerializer内部具有一个已知类型的列表,我们需要将Order类型添加到这个列表中。对于已知类型,可以通过两个特性设置:KnownTypeAttribute和ServiceKnownTypeAttribute。KnownTypeAttribute应用于数据契约中,用于设置继承于该数据契约类型的子数据契约,或引用其他的契约类型。ServiceKnownTypeAttribute既可以应用于服务契约的接口和方法上,还可以应用在服务实现的类和方法上,应用在不同的目标元素,决定了定义已知类型的作用范围,下面,通过在基类OrderBase指定了子契约的类型Order:
而ServiceKnownTypeAttribute特性,不仅可以使用在服务契约类型上,还可以应用在服务契约的操作方法上。如果应用在服务契约类型上,则已知类型在所有实现了该契约的服务操作中都有效,即作用范围为服务契约界别的,如果应用于服务契约的操作方法上,则定义的已知类型仅在实现了该契约的服务操作中有效。
```
// 服务契约
[ServiceContract]
[ServiceKnownType(typeof(Order))] // 服务契约级别
public interface IUserValidationService
{
// 为了演示已知类型的操作方法
//[OperationContract]
//[ServiceKnownType(typeof(Order))] // 单个服务操作级别
bool AddOrder(OrderBase order);
}
```
除了通过特性的方式设置已知类型外,还可以通过配置文件的方式来进行指定。已知类型定义在<System.runtime.serialization>配置节点中,可以采用下面的方式来定义:
```
```
## 六、总结
到这里,数据契约的分享就结束。对于这篇博文首先介绍了数据契约和序列化的基本知识,接着介绍了.NET中的序列化机制和WCF中序列化机制,最后完成了一个数据契约的例子。看完本篇文章应该明确几个问题:
1. SerializableAttribute与DataContract异同。
答:相同点:都是标记类型为可序列化类型
不同点:在于序列化的成员不一样,DataContract是Opt-in(明确参与)的方式,即使用DataMember特性明确标识哪些成员需要序列化,而Serializable是Opt-out方式,即使用NoSerializable特性明确标识不参与序列化的成员。
2\. BinartFormatter、DataContractSerializer和XmlSerializer的区别,具体答案见下图和参考下面博文:[XmlSerializer, DataContractSerializer 和 BinaryFormatter区别与用法分析](http://www.cnblogs.com/nankezhishi/archive/2012/05/12/serializationcompare.html)。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb489f366.png)
好的博文记录:[Create and Consume RESTFul Service in .NET Framework 4.0](http://www.codeproject.com/Articles/255684/Create-and-Consume-RESTFul-Service-in-NET-Framewor)
本文所有源代码下载:[WCFDataContract.zip](http://files.cnblogs.com/zhili/WCFDataContract.zip)
';
跟我一起学WCF(6)——深入解析服务契约 下篇
最后更新于:2022-04-02 00:13:34
# 跟我一起学WCF(6)——深入解析服务契约[下篇]
## 一、引言
在上一篇博文中,我们分析了如何在WCF中实现操作重载,其主要实现要点是服务端通过ServiceContract的Name属性来为操作定义一个别名来使操作名不一样,而在客户端是通过重写客户端代理类的方式来实现的。在这篇博文中将分享契约继承的实现。
## 二、WCF服务契约继承实现的限制
首先,介绍下WCF中传统实现契约继承的一个方式,下面通过一个简单的WCF应用来看看不做任何修改的情况下是如何实现契约继承的。我们还是按照之前的步骤来实现下这个WCF应用程序。
* **步骤一:实现WCF服务**
在这里,我们定义了两个服务契约,它们之间是继承的关系的,具体的实现代码如下所示:
```
1 // 服务契约
2 [ServiceContract]
3 public interface ISimpleInstrumentation
4 {
5 [OperationContract]
6 string WriteEventLog();
7 }
8
9 // 服务契约,继承于ISimpleInstrumentation这个服务契约
10 [ServiceContract]
11 public interface ICompleteInstrumentation :ISimpleInstrumentation
12 {
13 [OperationContract]
14 string IncreatePerformanceCounter();
15 }
```
上面定义了两个接口来作为服务契约,其中ICompleteInstrumentation继承ISimpleInstrumentation。这里需要注意的是:虽然ICompleteInstrumentation继承于ISimpleteInstrumentation,但是运用在ISimpleInstrumentation中的ServiceContractAttribute却不能被ICompleteInstrumentation继承,这是因为在它之上的AttributeUsage的Inherited属性设置为false,代表[ServiceContractAttribute](http://msdn.microsoft.com/zh-cn/library/system.servicemodel.servicecontractattribute(v=vs.110).aspx)不能被派生接口继承。ServiceContractAttribute的具体定义如下所示:
接下来实现对应的服务,具体的实现代码如下所示:
```
// 实现ISimpleInstrumentation契约
public class SimpleInstrumentationService : ISimpleInstrumentation
{
#region ISimpleInstrumentation members
public string WriteEventLog()
{
return "Simple Instrumentation Service is Called";
}
#endregion
}
// 实现ICompleteInstrumentation契约
public class CompleteInstrumentationService: SimpleInstrumentationService, ICompleteInstrumentation
{
public string IncreatePerformanceCounter()
{
return "Increate Performance Counter is called";
}
}
```
上面中,为了代码的重用,CompleteInstrumentationService继承自SimpleInstrumentationService,这样就不需要重新定义WriteEventLog方法了。
* **步骤二:实现服务宿主**
定义完成服务之后,现在就来看看服务宿主的实现,这里服务宿主是一个控制台应用程序,具体实现代码与前面几章介绍的代码差不多,具体的实现代码如下所示:
```
1 // 服务宿主的实现,把WCF服务宿主在控制台程序中
2 class Program
3 {
4 static void Main(string[] args)
5 {
6 using (ServiceHost host = new ServiceHost(typeof(WCFService.CompleteInstrumentationService)))
7 {
8 host.Opened += delegate
9 {
10 Console.WriteLine("Service Started");
11 };
12
13 // 打开通信通道
14 host.Open();
15 Console.Read();
16 }
17
18 }
19 }
```
宿主程序对应的配置文件信息如下所示:
```
```
* **步骤三:实现客户端**
最后,就是实现我们的客户端来对服务进行访问了,这里首先以管理员权限运行宿主应用程序,即以管理员权限运行WCFServiceHostByConsoleApp.exe可执行文件。运行成功之后,你将在控制台中看到服务启动成功的消息,具体运行结果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4865888.png)
然后在客户端通过添加服务引用的方式来添加服务引用,此时必须记住,一定要先运行宿主服务,这样才能在添加服务引用窗口中输入地址:http://localhost:9003/instrumentationService/ 才能获得服务的元数据信息。添加成功后,svcutil.exe工具除了会为我们生成对应的客户端代理类之前,还会自动添加配置文件信息,而且还会为我们添加System.ServiceModel.dll的引用。下面就是工具为我们生成的代码:
```
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="ServiceReference.ICompleteInstrumentation")]
public interface **ICompleteInstrumentation** {
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ISimpleInstrumentation/WriteEventLog", ReplyAction="http://tempuri.org/ISimpleInstrumentation/WriteEventLogResponse")]
string WriteEventLog();
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ISimpleInstrumentation/WriteEventLog", ReplyAction="http://tempuri.org/ISimpleInstrumentation/WriteEventLogResponse")]
System.Threading.Tasks.Task WriteEventLogAsync();
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ICompleteInstrumentation/IncreatePerformanceCounter", ReplyAction="http://tempuri.org/ICompleteInstrumentation/IncreatePerformanceCounterResponse")]
string IncreatePerformanceCounter();
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ICompleteInstrumentation/IncreatePerformanceCounter", ReplyAction="http://tempuri.org/ICompleteInstrumentation/IncreatePerformanceCounterResponse")]
System.Threading.Tasks.Task IncreatePerformanceCounterAsync();
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
public interface ICompleteInstrumentationChannel : ClientConsoleApp.ServiceReference.ICompleteInstrumentation, System.ServiceModel.IClientChannel {
}
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
public partial class **CompleteInstrumentationClient** : System.ServiceModel.ClientBase, ClientConsoleApp.ServiceReference.ICompleteInstrumentation {
public CompleteInstrumentationClient() {
}
public CompleteInstrumentationClient(string endpointConfigurationName) :
base(endpointConfigurationName) {
}
public CompleteInstrumentationClient(string endpointConfigurationName, string remoteAddress) :
base(endpointConfigurationName, remoteAddress) {
}
public CompleteInstrumentationClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress) {
}
public CompleteInstrumentationClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress) {
}
public string WriteEventLog() {
return base.Channel.WriteEventLog();
}
public System.Threading.Tasks.Task WriteEventLogAsync() {
return base.Channel.WriteEventLogAsync();
}
public string IncreatePerformanceCounter() {
return base.Channel.IncreatePerformanceCounter();
}
public System.Threading.Tasks.Task IncreatePerformanceCounterAsync() {
return base.Channel.IncreatePerformanceCounterAsync();
}
}
```
在服务端,我们定义了具有继承层次结构的服务契约,并为ICompleteInstrumentation契约公开了一个EndPoint。但是在客户端,我们通过添加服务引用的方式生成的服务契约却没有了继承的关系,在上面代码标红的地方可以看出,此时客户端代理类中只定义了一个服务契约,在该服务契约定义了所有的Operation。此时客户端的实现代码如下所示:
```
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 Console.WriteLine("---Use Genergate Client by VS Tool to call method of WCF service---");
6 using (CompleteInstrumentationClient proxy = new CompleteInstrumentationClient())
7 {
8 Console.WriteLine(proxy.WriteEventLog());
9 Console.WriteLine(proxy.IncreatePerformanceCounter());
10 }
11
12 Console.Read();
13 }
14 }
```
从上面代码可以看出。虽然现在我们可以通过调用CompleteInstrumentationClient代理类来完成服务的调用,但是我们希望的是,客户端代理类也具有继承关系的契约结构。
## 三、实现客户端的契约层级
既然,自动生成的代码不能完成我们的需要,此时我们可以通过自定义的方式来定义自己的代理类。
* 第一步就是定义客户端的Service Contract。具体的自定义代码如下所示:
```
1 namespace ClientConsoleApp
2 {
3 // 自定义服务契约,使其保持与服务端一样的继承结果
4 [ServiceContract]
5 public interface ISimpleInstrumentation
6 {
7 [OperationContract]
8 string WriteEventLog();
9 }
10
11 [ServiceContract]
12 public interface ICompleteInstrumentation : ISimpleInstrumentation
13 {
14 [OperationContract]
15 string IncreatePerformanceCounter();
16 }
17 }
```
* 第二步:自定义两个代理类,具体的实现代码如下所示:
```
// 自定义代理类
public class SimpleInstrumentationClient : ClientBase, ISimpleInstrumentation
{
#region ISimpleInstrumentation Members
public string WriteEventLog()
{
return this.Channel.WriteEventLog();
}
#endregion
}
public class CompleteInstrumentationClient:SimpleInstrumentationClient, ICompleteInstrumentation
{
public string IncreatePerformanceCounter()
{
return this.Channel.IncreatePerformanceCounter();
}
}
```
对应的配置文件修改为如下所示:
```
```
* 第三步:实现客户端来进行服务调用,此时可以通过两个自定义的代理类来分别对两个服务契约对应的操作进行调用,具体的实现代码如下所示:
```
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 using (SimpleInstrumentationClient proxy1 = new SimpleInstrumentationClient())
6 {
7 Console.WriteLine(proxy1.WriteEventLog());
8 }
9 using (CompleteInstrumentationClient proxy2 = new CompleteInstrumentationClient())
10 {
11 Console.WriteLine(proxy2.IncreatePerformanceCounter());
12 }
13
14 Console.Read();
15 }
16 }
```
这样,通过重写代理类的方式,客户端可以完全以面向对象的方式调用了服务契约的方法,具体的运行效果如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-23_56a2eb4874d4e.png)
另外,如果你不想定义两个代理类的话,你也可以通过下面的方式来对服务契约进行调用,具体的实现步骤为:
* 第一步:同样是实现具有继承关系的服务契约,具体的实现代码与前面一样。
```
// 自定义服务契约,使其保持与服务端一样的继承结果
[ServiceContract]
public interface ISimpleInstrumentation
{
[OperationContract]
string WriteEventLog();
}
[ServiceContract]
public interface ICompleteInstrumentation : ISimpleInstrumentation
{
[OperationContract]
string IncreatePerformanceCounter();
}
```
* 第二步:配置文件修改。把客户端配置文件修改为如下所示:
```
```
* 第三步:实现客户端代码。具体的实现代码如下所示:
```
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 using (ChannelFactory simpleChannelFactory = new ChannelFactory("ISimpleInstrumentation"))
6 {
7 ISimpleInstrumentation simpleProxy = simpleChannelFactory.CreateChannel();
8 using (simpleProxy as IDisposable)
9 {
10 Console.WriteLine(simpleProxy.WriteEventLog());
11 }
12 }
13 using (ChannelFactory completeChannelFactor = new ChannelFactory("ICompleteInstrumentation"))
14 {
15 ICompleteInstrumentation completeProxy = completeChannelFactor.CreateChannel();
16 using (completeProxy as IDisposable)
17 {
18 Console.WriteLine(completeProxy.IncreatePerformanceCounter());
19 }
20 }
21
22 Console.Read();
23 }
24 }
```
其实,上面的实现代码原理与定义两个客户端代理类是一样的,只是此时把代理类放在客户端调用代码中实现。通过上面代码可以看出,要进行通信,主要要创建与服务端的通信信道,即Channel,上面的是直接通过ChannelFactory<T>的CreateChannel方法来创建通信信道,而通过定义代理类的方式是通过ClientBase<T>的Channel属性来获得当前通信信道,其在ClientBase类本身的实现也是通过ChannelFactory.CreateChannel方法来创建信道的,再把这个创建的信道赋值给Channel属性,以供外面进行获取创建的信道。所以说这两种实现方式的原理都是一样的,并且通过自动生成的代理类也是一样的原理。
## 四、总结
到这里,本篇文章分享的内容就结束了,本文主要通过自定义代理类的方式来对契约继承服务的调用。其实现思路与上一篇操作重载的实现思路是一样的,既然客户端自动生成的代码类不能满足需求,那就只能自定义来扩展了。到此,服务契约的分享也就告一段落了,后面的一篇博文继续分享WCF中数据契约。
本人所有源码下载:[WCFServiceContract2.zip](http://files.cnblogs.com/zhili/WCFServiceContract2.zip)。
';