走向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.ServiceModel
我們之前說過:在服務層設計中,我們準備采用”文檔消息模式”和”請求-響應模式”。而且所有的響應對象都有一些共性,那么我們就首先定義一個響應類的基類,然后其他的響應都從繼承它:
public abstract class Response
{
[DataMember]
public bool Success { get; set; }
[DataMember]
public string Message { get; set; }
}
相信大家在之前一些文章中,已經見過很多這樣的代碼了。下面就來定義兩個具體的響應類:PurchaseTicketResponse和ReserveTicketResponse..其中PurchaseTicketResponse就代表了:客戶端向服務端發起購買票的請求后,服務端發送給客戶端的響應的數據結構。而ReserveTicketResponse就代表了:客戶端向服務端發送請求后,服務端發送給客戶端的一個標識對象的數據結構,這個標識對象可以說是用來對這個客戶端本次的交易進行唯一的識別的。
PurchaseTicketResponse和ReserveTicketResponse代碼:
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; }
}
同上,下面添加兩個請求的對象,代表了客戶端向服務端發送請求的數據結構:
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:
public interface ITicketService
{
[OperationContract()]
ReserveTicketResponse ReserveTicket(ReserveTicketRequest reserveTicketRequest);
[OperationContract()]
PurchaseTicketResponse PurchaseTicket(PurchaseTicketRequest purchaseTicketRequest);
}
這個服務接口主要暴露兩個功能給客戶端:
ReserveTicket方法:服務端創建一個標識,并且返回響應給客戶端。
PurchaseTicket:購買真實的票,并且把結果發送給客戶端。
下面,我們來添加兩個擴展方法類的輔助類:TicketPurchaseExtensionMethods和TicketReservationExtensionMethods.這兩個擴展方法類負責把TicketReservation業務類和TicketPurchase業務類轉換為文檔消息的數據格式。如下:
{
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架構設計-第六章-服務層設計(中篇)),代碼實現如下:
{
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];
}
}
這個類在內存中保存了一個請求-響應的記錄字典。每次客戶端發送一個請求,服務端就會去這個內存的字典中檢查這個請求是否已經被處理(檢查這個請求的預約標識是否在字典中存在),如果這個請求已經被處理了,那么服務端直接就不用在處理。當然,我們可以把”請求-響應”的處理記錄保存在其他的存儲介質中。
服務實現
下面就實現之前的服務契約實現代碼(代碼可能有點多,我們下面會做詳細的講解):
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方法:
{
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接口的類:
{
_eventRepository = eventRepository;
}
public TicketService() : this (new EventRepository()) // Poor mans DI
{ }
在上面的代碼中,為了示例的簡單,我們直接把EventRepository Hard Code傳入,當然了,可以采用IoC的方式來做,這里就暫不演示,大家可以參看之前的系列文章中的一些例子。
3、對于服務類的ReserveTicket方法,這個方法只有唯一的一個參數:ReserveTicketRequest,沒有像以前那樣傳入N多個參數。這個方法的作用就是標識客戶端的這個請求,為這個請求生成唯一的一個標識。因為可能接下來的一些客戶端的操作都屬于一個業務事務,一個流程可能很復雜,需要客戶端和服務端交互多次,但是這些多次交互都必須被看成是一個單元,所以,余下的請求都要帶上這個唯一的標識,表示它們是一體的。當然,我們這里的例子很簡單,沒有反應出來。
ReserveTicket方法檢查服務端是否還有足夠的票來滿足這個請求,并且把結果轉換為響應格式返回,并且通過標識Success來告訴客戶端請求的處理狀況。
{
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)
<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>
今天就介紹到這里,下一篇文章接著介紹。