走向ASP.NET架构设计——第七章:阶段总结,实践篇(中篇)
服务层(中篇)
上一篇文章中,我们已经讲述了业务逻辑层和数据访问层层的设计和编码,下面我们就来讲述服务层的设计。如我们之前所讨论的:服务层想客户端暴露简单易用的API.
如下图所示:
在上图中:
1. ASPPatterns.Chap6.EventTickets.Contract: 这个类库中定义了服务层的接口契约。
2. ASPPatterns.Chap6.EventTickets.Service:这个类库中包含了上面接口契约的实现类以及业务逻辑的协调和数据的持久化和返回数据
3. ASPPatterns.Chap6.EventTickets.DataContract:这个类库中包含了客户端和服务端的数据契约对象;而且客户端 服务端之前采用”文档消息模式”来交换数据。
4. ASPPatterns.Chap6.EventTickets.HTTPHost:这个类库中host了WCF的服务。
下面就从数据契约开始讲述,其实这也是在定义服务的时候一个很重要的思想:契约优先(服务契约和数据契约)。
数据契约
在设计服务层的时候,首先就要定义出客户端和服务端的数据交换的结构和格式,要定出数据的scheme.
因为我们用WCF为例子,那么我们在数据契约的类库中引入:
System.Runtime.Serialization
System.ServiceModel
我们之前说过:在服务层设计中,我们准备采用”文档消息模式”和”请求-响应模式”。而且所有的响应对象都有一些共性,那么我们就首先定义一个响应类的基类,然后其他的响应都从继承它:
[DataContract]
public abstract class Response
{
[DataMember]
public bool Success { get; set; }
[DataMember]
public string Message { get; set; }
}
相信大家在之前一些文章中,已经见过很多这样的代码了。下面就来定义两个具体的响应类:PurchaseTicketResponse和ReserveTicketResponse..其中PurchaseTicketResponse就代表了:客户端向服务端发起购买票的请求后,服务端发送给客户端的响应的数据结构。而ReserveTicketResponse就代表了:客户端向服务端发送请求后,服务端发送给客户端的一个标识对象的数据结构,这个标识对象可以说是用来对这个客户端本次的交易进行唯一的识别的。
PurchaseTicketResponse和ReserveTicketResponse代码:
[DataContract]
public class PurchaseTicketResponse : Response
{
[DataMember]
public string TicketId { get; set; }
[DataMember]
public String EventName { get; set; }
[DataMember]
public String EventId { get; set; }
[DataMember]
public int NoOfTickets { get; set; }
}
[DataContract]
public class ReserveTicketResponse : Response
{
[DataMember]
public string ReservationNumber {get; set;}
[DataMember]
public DateTime ExpirationDate { get; set; }
[DataMember]
public String EventName { get; set; }
[DataMember]
public String EventId { get; set; }
[DataMember]
public int NoOfTickets { get; set; }
}
同上,下面添加两个请求的对象,代表了客户端向服务端发送请求的数据结构:
[DataContract]
public class ReserveTicketRequest
{
[DataMember]
public string EventId { get; set; }
[DataMember]
public int TicketQuantity { get; set; }
}
[DataContract]
public class PurchaseTicketRequest
{
[DataMember]
public string CorrelationId { get; set; }
[DataMember]
public string ReservationId { get; set; }
[DataMember]
public string EventId { get; set; }
}
服务契约
定义完了数据契约之后,我们接下来定义服务接口契约ITicketService:
[ServiceContract(Namespace = "http://ASPPatterns.Chap6.EventTickets/")]
public interface ITicketService
{
[OperationContract()]
ReserveTicketResponse ReserveTicket(ReserveTicketRequest reserveTicketRequest);
[OperationContract()]
PurchaseTicketResponse PurchaseTicket(PurchaseTicketRequest purchaseTicketRequest);
}
这个服务接口主要暴露两个功能给客户端:
ReserveTicket方法:服务端创建一个标识,并且返回响应给客户端。
PurchaseTicket:购买真实的票,并且把结果发送给客户端。
下面,我们来添加两个扩展方法类的辅助类:TicketPurchaseExtensionMethods和TicketReservationExtensionMethods.这两个扩展方法类负责把TicketReservation业务类和TicketPurchase业务类转换为文档消息的数据格式。如下:
public static class TicketPurchaseExtensionMethods
{
public static PurchaseTicketResponse ConvertToPurchaseTicketResponse(this TicketPurchase ticketPurchase)
{
PurchaseTicketResponse response = new PurchaseTicketResponse();
response.TicketId = ticketPurchase.Id.ToString();
response.EventName = ticketPurchase.Event.Name;
response.EventId = ticketPurchase.Event.Id.ToString();
response.NoOfTickets = ticketPurchase.TicketQuantity;
return response;
}
}
public static class TicketReservationExtensionMethods
{
public static ReserveTicketResponse ConvertToReserveTicketResponse(this TicketReservation ticketReservation)
{
ReserveTicketResponse response = new ReserveTicketResponse();
response.EventId = ticketReservation.Event.Id.ToString();
response.EventName = ticketReservation.Event.Name;
response.NoOfTickets = ticketReservation.TicketQuantity;
response.ExpirationDate = ticketReservation.ExpiryTime;
response.ReservationNumber = ticketReservation.Id.ToString();
return response;
}
}
之前提到过:为了避免客户端因重复提交而导致服务端数据不一致,采用Idempotent Messaging模式(具体讲述,请参见走向ASP.NET架构设计-第六章-服务层设计(中篇)),代码实现如下:
public class MessageResponseHistory<T>
{
private Dictionary<string, T> _responseHistory;
public MessageResponseHistory()
{
_responseHistory = new Dictionary<string, T>();
}
public bool IsAUniqueRequest(string correlationId)
{
return !_responseHistory.ContainsKey(correlationId);
}
public void LogResponse(string correlationId, T response)
{
if (_responseHistory.ContainsKey(correlationId))
_responseHistory[correlationId] = response;
else
_responseHistory.Add(correlationId, response);
}
public T RetrievePreviousResponseFor(string correlationId)
{
return _responseHistory[correlationId];
}
}
这个类在内存中保存了一个请求-响应的记录字典。每次客户端发送一个请求,服务端就会去这个内存的字典中检查这个请求是否已经被处理(检查这个请求的预约标识是否在字典中存在),如果这个请求已经被处理了,那么服务端直接就不用在处理。当然,我们可以把”请求-响应”的处理记录保存在其他的存储介质中。
服务实现
下面就实现之前的服务契约实现代码(代码可能有点多,我们下面会做详细的讲解):
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class TicketService : ITicketService
{
private IEventRepository _eventRepository;
private static MessageResponseHistory<PurchaseTicketResponse> _reservationResponse = new MessageResponseHistory<PurchaseTicketResponse>();
public TicketService(IEventRepository eventRepository)
{
_eventRepository = eventRepository;
}
public TicketService() : this (new EventRepository()) // Poor mans DI
{ }
public ReserveTicketResponse ReserveTicket(ReserveTicketRequest reserveTicketRequest)
{
ReserveTicketResponse response = new ReserveTicketResponse();
try
{
Event Event = _eventRepository.FindBy(new Guid(reserveTicketRequest.EventId));
TicketReservation reservation;
if (Event.CanReserveTicket(reserveTicketRequest.TicketQuantity) )
{
reservation = Event.ReserveTicket(reserveTicketRequest.TicketQuantity);
_eventRepository.Save(Event);
response = reservation.ConvertToReserveTicketResponse();
response.Success = true;
}
else
{
response.Success = false;
response.Message = String.Format("There are {0} ticket(s) available.", Event.AvailableAllocation());
}
}
catch (Exception ex)
{
// Shield Exceptions
response.Message = ErrorLog.GenerateErrorRefMessageAndLog(ex);
response.Success = false;
}
return response;
}
public PurchaseTicketResponse PurchaseTicket(PurchaseTicketRequest PurchaseTicketRequest)
{
PurchaseTicketResponse response = new PurchaseTicketResponse();
try
{
// Check for a duplicate transaction using the Idempotent Pattern,
// the Domain Logic could cope but we can't be sure.
if (_reservationResponse.IsAUniqueRequest(PurchaseTicketRequest.CorrelationId))
{
TicketPurchase ticket;
Event Event = _eventRepository.FindBy(new Guid(PurchaseTicketRequest.EventId));
if (Event.CanPurchaseTicketWith(new Guid(PurchaseTicketRequest.ReservationId)))
{
ticket = Event.PurchaseTicketWith(new Guid(PurchaseTicketRequest.ReservationId));
_eventRepository.Save(Event);
response = ticket.ConvertToPurchaseTicketResponse();
response.Success = true;
}
else
{
response.Message = Event.DetermineWhyATicketCannotbePurchasedWith(new Guid(PurchaseTicketRequest.ReservationId));
response.Success = false;
}
_reservationResponse.LogResponse(PurchaseTicketRequest.CorrelationId, response);
}
else
{
// Retrieve last response
response = _reservationResponse.RetrievePreviousResponseFor(PurchaseTicketRequest.CorrelationId);
}
}
catch (Exception ex)
{
// Shield Exceptions
response.Message = ErrorLog.GenerateErrorRefMessageAndLog(ex);
response.Success = false;
}
return response;
}
}
1、TicketService类包含了一个MessageResponseHistory对象的引用,这样就确保了所有调用这个服务的请求的响应都被记录下来。当一个请求发送到了服务端之后,在对应的服务方法里面就检查这个请求是否已经被处理,如下PurchaseTicket方法:
public PurchaseTicketResponse PurchaseTicket(PurchaseTicketRequest PurchaseTicketRequest)
{
PurchaseTicketResponse response = new PurchaseTicketResponse();
try
{
// Check for a duplicate transaction using the Idempotent Pattern,
// the Domain Logic could cope but we can't be sure.
if (_reservationResponse.IsAUniqueRequest(PurchaseTicketRequest.CorrelationId))
{
TicketPurchase ticket;
Event Event = _eventRepository.FindBy(new Guid(PurchaseTicketRequest.EventId));
if (Event.CanPurchaseTicketWith(new Guid(PurchaseTicketRequest.ReservationId)))
{
ticket = Event.PurchaseTicketWith(new Guid(PurchaseTicketRequest.ReservationId));
_eventRepository.Save(Event);
response = ticket.ConvertToPurchaseTicketResponse();
response.Success = true;
}
else
{
response.Message = Event.DetermineWhyATicketCannotbePurchasedWith(new Guid(PurchaseTicketRequest.ReservationId));
response.Success = false;
}
_reservationResponse.LogResponse(PurchaseTicketRequest.CorrelationId, response);
}
else
{
// Retrieve last response
response = _reservationResponse.RetrievePreviousResponseFor(PurchaseTicketRequest.CorrelationId);
}
}
catch (Exception ex)
{
// Shield Exceptions
response.Message = ErrorLog.GenerateErrorRefMessageAndLog(ex);
response.Success = false;
}
return response;
}
2、这个服务类有两个构造函数:第一个是无参,第二个需要传入一个实现了IEventRepository接口的类:
public TicketService(IEventRepository eventRepository)
{
_eventRepository = eventRepository;
}
public TicketService() : this (new EventRepository()) // Poor mans DI
{ }
在上面的代码中,为了示例的简单,我们直接把EventRepository Hard Code传入,当然了,可以采用IoC的方式来做,这里就暂不演示,大家可以参看之前的系列文章中的一些例子。
3、对于服务类的ReserveTicket方法,这个方法只有唯一的一个参数:ReserveTicketRequest,没有像以前那样传入N多个参数。这个方法的作用就是标识客户端的这个请求,为这个请求生成唯一的一个标识。因为可能接下来的一些客户端的操作都属于一个业务事务,一个流程可能很复杂,需要客户端和服务端交互多次,但是这些多次交互都必须被看成是一个单元,所以,余下的请求都要带上这个唯一的标识,表示它们是一体的。当然,我们这里的例子很简单,没有反应出来。
ReserveTicket方法检查服务端是否还有足够的票来满足这个请求,并且把结果转换为响应格式返回,并且通过标识Success来告诉客户端请求的处理状况。
public ReserveTicketResponse ReserveTicket(ReserveTicketRequest reserveTicketRequest)
{
ReserveTicketResponse response = new ReserveTicketResponse();
try
{
Event Event = _eventRepository.FindBy(new Guid(reserveTicketRequest.EventId));
TicketReservation reservation;
if (Event.CanReserveTicket(reserveTicketRequest.TicketQuantity) )
{
reservation = Event.ReserveTicket(reserveTicketRequest.TicketQuantity);
_eventRepository.Save(Event);
response = reservation.ConvertToReserveTicketResponse();
response.Success = true;
}
else
{
response.Success = false;
response.Message = String.Format("There are {0} ticket(s) available.", Event.AvailableAllocation());
}
}
catch (Exception ex)
{
// Shield Exceptions
response.Message = ErrorLog.GenerateErrorRefMessageAndLog(ex);
response.Success = false;
}
return response;
}
4、PurchaseTicket方法和之前的ReserveTicket方法不同,这个方法就是真正的用来订票的,之前的ReserveTicket只是验证和产生请求标识的。PurchaseTicket方法首先检查请求中的标识是否存在了响应了,即是否被处理过了,如果没有处理,就开始购票的流程,最后产生响应结果,并且保存响应。如果已经处理过了,直接返回。
宿主程序
下面一步就是把这个服务运行起来,方式有很多,这里我们就把整个服务host在IIS中。
首先在ASPPatterns.Chap6.EventTicket.HTTPHost类库中添加一个TicketService.svc.,并且修改这个文件的代码:如下:
然后在web.config中添加wcf的一些配置:就是我们常常说的”ABC”(Address, Binding, Contract)
<system.serviceModel>
<services>
<service name="ASPPatterns.Chap6.EventTickets.Service.TicketService" behaviorConfiguration="metadataBehavior">
<endpoint address="" binding="wsHttpBinding" contract="ASPPatterns.Chap6.EventTickets.Contracts.ITicketService" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="metadataBehavior">
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
</system.serviceModel>
今天就介绍到这里,下一篇文章接着介绍。