一起谈.NET技术,走向ASP.NET架构设计——第七章:阶段总结,实践篇(中篇)

简介:   服务层(中篇)  上一篇文章中,我们已经讲述了业务逻辑层和数据访问层层的设计和编码,下面我们就来讲述服务层的设计。如我们之前所讨论的:服务层想客户端暴露简单易用的API.  如下图所示:  在上图中:1. ASPPatterns.Chap6.EventTickets.Contract: 这个类库中定义了服务层的接口契约。

  服务层(中篇)

  上一篇文章中,我们已经讲述了业务逻辑层和数据访问层层的设计和编码,下面我们就来讲述服务层的设计。如我们之前所讨论的:服务层想客户端暴露简单易用的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 >

  今天就介绍到这里,下一篇文章接着介绍。

目录
相关文章
|
8月前
|
监控 Cloud Native 测试技术
.NET技术深度解析:现代企业级开发指南
每日激励:“不要一直责怪过去的自己,他曾经站在雾里也很迷茫”。我是蒋星熠Jaxonic,一名在代码宇宙中探索的极客旅人。从.NET Framework到.NET 8,我深耕跨平台、高性能、云原生开发,践行领域驱动设计与微服务架构,用代码书写技术诗篇。分享架构演进、性能优化与AI融合前沿,助力开发者在二进制星河中逐光前行。关注我,共探技术无限可能!
.NET技术深度解析:现代企业级开发指南
|
8月前
|
人工智能 API 数据库
Semantic Kernel .NET 架构学习指南
本指南系统解析微软Semantic Kernel .NET架构,涵盖核心组件、设计模式与源码结构,结合实战路径与调试技巧,助你从入门到贡献开源,掌握AI编排开发全栈技能。
952 2
|
开发框架 前端开发 .NET
一个适用于 .NET 的开源整洁架构项目模板
一个适用于 .NET 的开源整洁架构项目模板
362 26
|
SQL 小程序 API
如何运用C#.NET技术快速开发一套掌上医院系统?
本方案基于C#.NET技术快速构建掌上医院系统,结合模块化开发理念与医院信息化需求。核心功能涵盖用户端的预约挂号、在线问诊、报告查询等,以及管理端的排班管理和数据统计。采用.NET Core Web API与uni-app实现前后端分离,支持跨平台小程序开发。数据库选用SQL Server 2012,并通过读写分离与索引优化提升性能。部署方案包括Windows Server与负载均衡设计,确保高可用性。同时针对API差异、数据库老化及高并发等问题制定应对措施,保障系统稳定运行。推荐使用Postman、Redgate等工具辅助开发,提升效率与质量。
594 0
|
开发框架 算法 .NET
C#/.NET/.NET Core技术前沿周刊 | 第 15 期(2024年11.25-11.30)
C#/.NET/.NET Core技术前沿周刊 | 第 15 期(2024年11.25-11.30)
305 6
|
开发框架 Cloud Native .NET
C#/.NET/.NET Core技术前沿周刊 | 第 16 期(2024年12.01-12.08)
C#/.NET/.NET Core技术前沿周刊 | 第 16 期(2024年12.01-12.08)
347 6
|
机器学习/深度学习 人工智能 Cloud Native
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台。本文深入解析 .NET 的核心优势,探讨其在企业级应用、Web 开发及移动应用等领域的应用案例,并展望未来在人工智能、云原生等方面的发展趋势。
465 3
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
888 0
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
445 7