学生数据库:ASP.NET MVC、WebAPI、EF Codefirst、Task、ESB 展示






4.94/5 (35投票s)
一个示例 Web 应用程序,展示了 ASP.NET MVC、WebAPI、EF 以及 ESB 的强大功能,用于构建一个安全、可靠、基于 REST 的 Web API 和 Web 前端。
1 摘要
想象一下:你是牛津大学的学生,想加入剑桥大学的一个项目。剑桥大学希望获取你在牛津大学完成的所有课程以及成绩,并自动将其计入学分,这样你就不必重复这些课程。最终,当剑桥大学决定授予你学位时,他们可以自动检查你是否完成了其项目要求的所有课程,无论是在剑桥、牛津、哈佛、麻省理工学院还是世界上任何其他大学。如果有一个通用系统,可以安全地存储你来自世界任何大学的所有课程和成绩,并且大学之间可以完全自动地互相交换你的学习记录,那岂不是很好?这样就不再需要打印、邮寄、认证成绩单等,一切都可以完全在线安全地完成。
让我们使用 ASP.NET MVC 和 WebAPI 构建这样一个虚拟系统,并通过 WSO2 ESB 等 ESB 暴露该系统。你不必使用任何 ESB,可以直接运行 ASP.NET 应用程序。ESB 的作用是提供安全、可靠的服务暴露。
让我们定义一下我们希望这个系统具备哪些功能
StudentDataBank.org (SDB – 一个虚拟组织) 是一个组织,提供大学、学院、学校和其他类型的教育机构 (EI) 之间学生记录的安全在线存储和受控交换。一个 EI 可以与学生数据库建立安全接口,并上传其课程、项目、学生和学生的课程成绩。然后,另一个 EI 可以请求从提供 EI 获取学生的记录。学生数据库 (SDB) 负责安全验证请求,并通过 EI 已与 SDB 建立的安全接口从 EI 系统中获取学生记录。因此,SDB 作为多个 EI 之间的经纪人,使每个 EI 都能以安全、可审计的方式相互共享学生记录。
一个授予学位的 EI 可以启动授予学生学位的过程,并在此过程中自动确定获得学位的学生是否已完成授予学位的 EI 以及学生声称曾就读的其他 EI 要求的所有课程。每个 EI 都有办法定义其课程满足哪些要求,以便在授予学生学位时可以自动接受其他 EI 的课程。
2 术语
- SDB – Student Data Bank – 解决方案的名称。
- EI – Educational Institute – 可以是学校、学院、大学甚至培训机构。
- Endpoint – 可以是 REST 或 SOAP Web 服务,或通过安全 FTP/RPC 暴露的接口。
- ESB – 企业服务总线。
- BPS – 业务流程服务器。
- Process – 托管在 BPS 上的同步或异步工作流。
3 挑战
本文将向你展示如何应对以下挑战
- 构建你的分层数据模型的完全 RESTful 暴露。例如:/Students、/Students/Omar/Courses、/Students/Omar/Courses/MAT101/Results
- 从 Web API 生成干净、可读的 XML/JSON 输出,特别是对于集合,而不包含 .NET 序列化内容。
- 通过 REST 进行异步长时间运行的作业处理。
- Web API 的复杂路径和控制器映射。
- 使用 Entity Framework Code First。
- 使用 Visual Studio 的自动化测试功能对 REST API 进行自动化集成测试。
- 最大限度地利用 ESB 实现安全性、缓存、性能、集成。
- 通过 ESB 暴露服务。
- 提供 REST API 的 SOAP 暴露,以方便拥有强类型 SOAP 客户端。
- 此类项目的架构和设计考虑。
4 交付物
原型代码已上传到 Github
https://github.com/oazabir/StudentDataBank
运行 Web 应用程序的软件先决条件
- SQL Server 2012 Express with LocalDB:http://msdn.microsoft.com/en-gb/evalcenter/hh230763.aspx
- Visual Studio 2013:http://www.asp.net/get-started
- 可选:WSO2 ESB 4.5.1 用于 SOAP 2 REST 代理:http://wso2.com/products/enterprise-service-bus/
只需在 Visual Studio 2013(或更高版本)中加载解决方案并点击运行。
5 架构
5.1 高层架构
学生数据库 (SDB) 将 EI 提供的学生记录存储在数据库中,并通过 REST API 暴露这些数据。API 访问仅限于与 SDB 签订合同并已从 SDB 获取客户端证书的 EI,以防止未经授权访问学生的私人记录。EI 还可以向 SDB 提供其系统的接口,允许 SDB 发送请求以验证学生,以检查学生是否在该 EI 注册。该接口还允许 SDB 获取学生在该 EI 完成的课程,以便 SDB 可以根据请求与其他 EI 共享这些数据。
SDB 有一个通过 HTTPS 暴露的网站,已获得凭据的 EI 管理员用户可以登录,以管理课程、项目和学生记录。
SDB 使用企业服务总线 (ESB) 来实现总线架构,并管理 REST API 对内部系统和外部 EI 的暴露。它使用业务流程服务器 (BPS) 运行编排内部 REST API 和外部 EI 服务的流程。
图 1 高层架构
架构亮点
- 网站通过 HTTPS 对 EI 管理用户和学生开放。EI 管理用户可以使用网站管理 SDB 上的课程、项目和学生记录。学生可以创建自己的个人资料并声明属于某个 EI。EI 收到该声明后,如果批准,学生就可以查看该 EI 在 SDB 中存储的其记录。
- 每个 EI 都可以与 SDB 建立安全接口,SDB 使用该接口与 EI 通信。ESB 维护每个 EI 服务的端点,并使用客户端证书向 EI 端点进行身份验证。每个 EI 端点的定义和策略存储在治理注册表中。每个 EI 都可以通过安全接口提供服务,以验证声称是其学生的学生,一旦验证成功,就向 SDB 提供该学生已修的课程。
- SDB 使用业务流程服务器运行两个流程——验证学生声明流程和获取学生记录流程。这些流程通过 ESB 与 EI 端点通信。这些流程包含EI 特定的编排逻辑。例如,一个 EI 可能提供直接的 HTTPS 服务来获取学生记录,而另一个 EI 可能需要登录才能访问学生记录。此外,每个 EI 都有自己的学生记录暴露格式。因此,流程使用 XSLT 转换将 EI 特定的响应格式转换为 SDB 的 XML 格式。同样,一个 EI 可能根本没有接口,需要 EI 管理部门的手动批准和手动上传学生记录。所有这些 EI 特定的编排逻辑都保存在 BPS 流程中。在 EI 入驻流程中,一旦 EI 与 SDB 签署协议,SDB 管理员就会建立特定于 EI 的证书、编排逻辑和 XSLT 转换逻辑。BPS 还通过 ESB 上的非安全内部暴露使用 REST API。
- ESB 在 SDB 内部充当总线。首先,它提供 REST API 的日志记录、监控、限流、缓存。其次,它通过安全的计量接口向希望使用自己的前端来管理 SDB 中存储的记录的 EI 暴露 REST API 的特定部分。安全暴露应用策略,以确保 EI 前端只能修改 EI 自己提供的数据,而不能修改其他 EI 存储的数据。每个 EI 从 SDB 管理部门获取客户端证书,EI 使用该证书进行身份验证和加密 SDB API 调用。
5.2 低层设计
以下组件图展示了 SDB 架构如何与其他 EI 协同工作。场景是:一所授予学位的大学希望授予一名在其他大学修过课程的学生学位。授予学位的大学首先希望验证该学生是否确实属于其他声称的大学,然后获取在该大学完成的课程。
架构亮点
- SDB 网站或大学的注册系统可以消费 ESB 暴露的 REST 数据服务。ESB 暴露了 API 中非常特定的服务,允许外部大学系统执行非常特定的操作,例如将学生添加到同一大学、向学生授予某个项目的学位、只读取其他大学的数据等。此外,ESB 使用客户端证书验证客户端,客户端证书对客户端进行身份验证,并对 API 消费进行限流、日志记录和监控。
- 对于复杂的操作,BPS 流程通过 ESB 暴露为 SOAP 服务。例如,验证学生声称是某大学学生的操作通过暴露为 SOAP 服务的验证流程完成。
- BPS 流程通过 ESB 内部消费 REST 数据服务。它们还通过 ESB 与外部 EI 端点通信。治理注册表维护每个 EI 的策略、证书、端点定义,因此如果需要任何更改,可以集中更改,并且 ESB 和 BPS 都可以获取最新的定义。此外,ESB 记录所有来自 EI 端点的响应,以帮助解决任何争议。
6 模式和消息格式
6.1 实体图
图 2 学生数据库实体模型
教育机构 (EducationalInstitute) 提供项目 (Programs),例如软件工程硕士,以及课程 (Courses),例如 MAT101、MAT102、PHY101 等。EI 定义哪些课程是哪些项目所必需的。EI 还可以将其提供的课程与一个主通用课程 (UniversalCourse) 进行映射。通用课程是 SDB 范围内的唯一课程定义,唯一标识一门课程及其要求。例如,SDB 定义 MAT101_BASIC,这是一门基础数学 101 课程,具有预定义的教学主题集。现在牛津大学可以决定将其 MAT101 课程映射到这个主 MAT101_BASIC 通用课程。然后,如果剑桥大学决定将其 MAT102 课程映射到这个主 MAT101_BASIC 通用课程,那么在牛津大学修过 MAT101 的学生将自动在剑桥大学获得 MAT102 的学分,反之亦然。SDB 通过与教育机构讨论来管理这个主通用课程,以便它可以提供全球范围内教授的独特课程的全面集合,并促进教育机构之间的课程自动映射。
6.2 实体定义
REST API 同时支持 XML 和 JSON。根据请求的 Accept 头,它会提供 XML 或 JSON。
6.2.1 EducationalInstitute
定义一个单独的教育机构,例如大学、学院或学校。是主键。
<EducationalInstitute>
<Address>Oxfordshire, UK</Address>
<Code>OX</Code>
<Name>Oxford University</Name>
</EducationalInstitute>
JSON 表示形式为
{"Code": "OX",
"Name": "Oxford University",
"Address": "Oxfordshire, UK"}
6.2.2 教育机构提供的课程
定义教育机构为其学生提供的项目。
<Programs>
<Program>
<Code>MSC_SE</Code>
<Name>Masters in Software Engineering</Name>
</Program>
<Program>
<Code>MSC_PHY</Code>
<Name>Masters in Physics</Name>
</Program>
</Programs>
6.2.3 教育机构的学生
每个教育机构都包含其学生的集合,其中每个学生都有一个特定于机构的学生 ID。
<Students>
<Student>
<Firstname>Omar</Firstname>
<Lastname>AL Zabir</Lastname>
<StudentId>OX123</StudentId>
</Student>
</Students>
6.2.4 学生在教育机构内注册的课程
学生注册教育机构提供的项目。一旦学生满足了项目所有必修课程的要求,学位就会授予给该学生。
<StudentPrograms>
<ProgramEnrolled>
<CGPA>3.5</CGPA>
<EndDate>2001-03-03T00:00:00</EndDate>
<LastRefreshedAt>2013-01-26T15:27:58.86</LastRefreshedAt>
<ProgramCode>MSC_SE</ProgramCode>
<StartDate>2001-01-01T00:00:00</StartDate>
<Status>InProgress</Status>
</ProgramEnrolled>
</StudentPrograms>
6.2.5 计入学分的课程
在 EI 内完成的课程会自动计入学生注册的项目。此外,如果学生与其他 EI 有关联,在这些 EI 完成的课程也会被导入并自动计入授予 EI 内的一门课程。例如,牛津大学的学生可以在剑桥大学完成的课程计入牛津大学软件工程硕士学位。
<CoursesCredited>
<CourseCreditedTowardsProgram>
<CreditedCourseCode>MAT101</CreditedCourseCode>
<CreditedCourseEICode>CAM</CreditedCourseEICode>
<Grade>A</Grade>
<RequiredCourseCode>MAT101</RequiredCourseCode>
<Score>3.5</Score>
<Status>Accepted</Status>
</CourseCreditedTowardsProgram>
<CourseCreditedTowardsProgram>
<CreditedCourseCode>PROG101</CreditedCourseCode>
<CreditedCourseEICode>OX</CreditedCourseEICode>
<Grade>A</Grade>
<RequiredCourseCode>PROG101</RequiredCourseCode>
<Score>3.5</Score>
<Status>Accepted</Status>
</CourseCreditedTowardsProgram>
</CoursesCredited>
在上面的例子中,第一门课程来自剑桥大学并被接受为牛津大学(当前教育机构)硕士学位所需的MAT101课程。第二门课程在牛津大学内完成,并照常计入硕士学位。
7 服务
7.1 作为 REST API 的数据服务
7.1.1 内部不受限制的暴露
REST API 允许对 SDB 数据库中存储的数据进行读/写操作。它可用于创建/编辑/删除 EI 中的学生记录、启动授予学生项目、添加学生课程成绩等。例如,以下 URL 返回一个 EI 定义
c:\Java>curl https://:1657/api/institutes/OX
<EducationalInstitute xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://studentdatabank.org">
<Address>Oxfordshire, UK</Address>
<Code>OX</Code>
<Courses i:nil="true" />
<Name>Oxford University</Name>
<Programs i:nil="true" />
<StudentClaims i:nil="true" />
<Students i:nil="true" />
</EducationalInstitute>
教育机构有三个子集合——项目集合、学生声明和学生。将子集合的名称附加到 URL 将返回子集合数据。例如,附加 /Students 将返回属于该大学的学生
c:\Java>curl https://:1657/api/institutes/OX/Students <Students xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://studentdatabank.org"> <Student> <CoursesTaken i:nil="true" /> <Firstname>Omar</Firstname> <Lastname>AL Zabir</Lastname> <LinksToOtherEI i:nil="true" /> <Programs i:nil="true" /> <StudentId>OX123</StudentId> </Student> </Students>
要查看单个学生的详细信息,请附加学生 ID,例如 /
c:\Java>curl https://:1657/api/institutes/OX/Students/OX123 <Student xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://studentdatabank.org"> <CoursesTaken i:nil="true" /> <Firstname>Omar</Firstname> <Lastname>AL Zabir</Lastname> <LinksToOtherEI i:nil="true" /> <Programs i:nil="true" /> <StudentId>OX123</StudentId> </Student>
获取学生当前注册的课程,只需附加 /Programs
c:\Java>curl https://:1657/api/institutes/OX/Students/OX123/Programs <StudentPrograms xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://studentdatabank.org"> <ProgramEnrolled> <CGPA>3.5</CGPA> <CoursesCredited i:nil="true" /> <EndDate>2001-03-03T00:00:00</EndDate> <LastRefreshedAt>2013-01-26T17:21:10.503</LastRefreshedAt> <ProgramCode>MSC_SE</ProgramCode> <StartDate>2001-01-01T00:00:00</StartDate> <Status>InProgress</Status> </ProgramEnrolled> </StudentPrograms>
可以通过对 EI 的 /Students 集合进行 POST 操作来添加学生。例如,向牛津大学添加一名新学生需要
c:\Java>curl https://:1657/api/institutes/OX/Students -d "<Student xmlns=""http://studentdatabank.org""><Firstname>First Name</Firstname><Lastname>Last name</Lastname><StudentId>OX999</StudentId></Student>" -v -H "Content-Type: application/xml" > POST /api/institutes/OX/Students HTTP/1.1 > Content-Type: application/xml > Content-Length: 145 > * upload completely sent off: 145 out of 145 bytes < HTTP/1.1 201 Created < Content-Type: application/xml; charset=utf-8 < Location: https://:1657/api/institutes/OX/students/OX999 < Date: Sat, 26 Jan 2013 17:49:16 GMT < Content-Length: 308 < <Student xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://studentdatabank.org"> <CoursesTaken i:nil="true" /> <Firstname>First Name</Firstname> <Lastname>Last name</Lastname> <LinksToOtherEI i:nil="true" /> <Programs i:nil="true" /> <StudentId>OX999</StudentId> </Student>
同样,可以使用 PUT 更新学生,使用 DELETE 删除学生到学生的 URL。
7.1.2 外部受限暴露
数据服务的有限部分暴露给感兴趣的 EI,这些 EI 拥有自己的客户端系统,并希望向 SDB 读取/写入数据。ESB 确保只有拥有客户端证书的客户端才能在外部消费此服务。ESB 管理 REST API 对 EI 的加密、计量、限流暴露。
7.2 BPS SOAP 服务
有两个 BPS 流程通过 ESB 暴露为 SOAP 服务
- 验证学生声明流程 – 一个教育机构或学生本人可以声称是另一教育机构的学生。此流程验证该声明,如果成功,则在 SDB 数据库中创建学生记录。
- 获取学生记录流程 – 一个教育机构或学生本人可以请求从一个教育机构获取记录并存储在 SDB 数据库中。
这些流程以 SOAP 服务形式暴露。
7.2.1 验证学生声明流程
一个教育机构可以询问另一个教育机构某个学生是否属于它。如果目标机构与 SDB 建立了服务,那么该流程将调用该服务。否则,该流程会为目标机构的管理员用户添加一个手动任务以批准声明。所有此类机构特定的逻辑都设计为该流程中的异步工作流。一个示例实现可能如下所示
该服务的 WSDL 如下所示
该服务的 WSDL 是
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:vprop="http://docs.oasis-open.org/wsbpel/2.0/varprop" xmlns:plnk="http://docs.oasis-open.org/wsbpel/2.0/plnktype" xmlns:wsdl1="http://ws.apache.org/axis2" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://studentdatabank.org" xmlns:wsdl="http://tempuri.org/" name="ValidateStudent" targetNamespace="http://studentdatabank.org">
<import namespace="http://tempuri.org/" location="ValidateStudent?wsdl=OxfordAwardService.wsdl"></import>
<import namespace="http://ws.apache.org/axis2" location="ValidateStudent?wsdl=POSTSOAP2REST.wsdl"></import>
<types>
<schema xmlns="http://www.w3.org/2001/XMLSchema" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://studentdatabank.org">
<element name="ValidateStudentRequest">
<complexType>
<sequence>
<element name="universityCode" type="string"/>
<element name="studentId" type="string"/>
<element name="firstName" type="string"/>
<element name="lastName" type="string"/>
<element name="dateOfBirth" type="string"/>
<element name="gender" type="string"/>
</sequence>
</complexType>
</element>
<element name="ValidateStudentResponse">
<complexType>
<sequence>
<element name="result" type="boolean"/>
</sequence>
</complexType>
</element>
</schema>
</types>
<message name="ValidateStudentResponseMessage">
<part name="payload" element="tns:ValidateStudentResponse"></part>
</message>
<message name="ValidateStudentRequestMessage">
<part name="payload" element="tns:ValidateStudentRequest"></part>
</message>
<portType name="ValidateStudent">
<operation name="initiate">
<input message="tns:ValidateStudentRequestMessage"></input>
</operation>
</portType>
<portType name="ValidateStudentCallback">
<operation name="onResult">
<input message="tns:ValidateStudentResponseMessage"></input>
</operation>
</portType>
<binding name="ValidateStudentBinding" type="tns:ValidateStudent">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="initiate">
<soap:operation soapAction="http://studentdatabank.org/initiate"/>
<input>
<soap:body use="literal"/>
</input>
</operation>
</binding>
<binding name="ValidateStudentCallbackBinding" type="tns:ValidateStudentCallback">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="onResult">
<soap:operation soapAction="http://studentdatabank.org/onResult"/>
<input>
<soap:body use="literal"/>
</input>
</operation>
</binding>
<service name="ValidateStudent">
<port name="ValidateStudentPort" binding="tns:ValidateStudentBinding">
<soap:address location="http://192.168.1.70:9766/services/ValidateStudent/"/>
</port>
</service>
<service name="ValidateStudentCallback">
<port name="ValidateStudentPortCallbackPort" binding="tns:ValidateStudentCallbackBinding">
<soap:address location="http://192.168.1.70:9766/services/ValidateStudent/"/>
</port>
</service>
<plnk:partnerLinkType name="OxfordLinkType">
<plnk:role name="OxfordRole" portType="wsdl:IAwardService"/>
</plnk:partnerLinkType>
<plnk:partnerLinkType name="DataServiceProxyLinkType">
<plnk:role name="DataServiceRole" portType="wsdl1:POSTSOAP2RESTPortType"/>
</plnk:partnerLinkType>
<plnk:partnerLinkType name="ValidateStudent">
<plnk:role name="ValidateStudentProvider" portType="tns:ValidateStudent"/>
<plnk:role name="ValidateStudentRequester" portType="tns:ValidateStudentCallback"/>
</plnk:partnerLinkType>
</definitions>
服务的示例请求如下
<p:ValidateStudentRequest xmlns:p="http://studentdatabank.org"> <!--Exactly 1 occurrence--> <universityCode xmlns="http://studentdatabank.org">?</universityCode> <!--Exactly 1 occurrence--> <studentId xmlns="http://studentdatabank.org">?</studentId> <!--Exactly 1 occurrence--> <firstName xmlns="http://studentdatabank.org">?</firstName> <!--Exactly 1 occurrence--> <lastName xmlns="http://studentdatabank.org">?</lastName> <!--Exactly 1 occurrence--> <dateOfBirth xmlns="http://studentdatabank.org">?</dateOfBirth> <!--Exactly 1 occurrence--> <gender xmlns="http://studentdatabank.org">?</gender> </p:ValidateStudentRequest>
7.2.2 获取学生记录流程
此流程要么调用 EI 的服务以从 EI 获取学生记录,要么为 EI 的管理员用户排队一个手动任务以执行。
对此服务的请求仅仅是 UniversityCode 和 StudentID
该服务的 WSDL 是
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:vprop="http://docs.oasis-open.org/wsbpel/2.0/varprop" xmlns:plnk="http://docs.oasis-open.org/wsbpel/2.0/plnktype" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://studentdatabank.org" xmlns:wsdl="http://tempuri.org/" name="FetchStudentRecord" targetNamespace="http://studentdatabank.org">
<import namespace="http://tempuri.org/" location="FetchStudentRecord?wsdl=OxfordAwardService.wsdl"></import>
<types>
<schema xmlns="http://www.w3.org/2001/XMLSchema" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://studentdatabank.org">
<element name="FetchStudentRecordRequest">
<complexType>
<sequence>
<element name="universityCode" type="string"/>
<element name="studentId" type="string"/>
</sequence>
</complexType>
</element>
<element name="FetchStudentRecordResponse">
<complexType>
<sequence>
<element name="result" type="string"/>
</sequence>
</complexType>
</element>
</schema>
</types>
<message name="FetchStudentRecordRequestMessage">
<part name="payload" element="tns:FetchStudentRecordRequest"></part>
</message>
<message name="FetchStudentRecordResponseMessage">
<part name="payload" element="tns:FetchStudentRecordResponse"></part>
</message>
<portType name="FetchStudentRecord">
<operation name="process">
<input message="tns:FetchStudentRecordRequestMessage"></input>
<output message="tns:FetchStudentRecordResponseMessage"></output>
</operation>
</portType>
<binding name="FetchStudentRecordBinding" type="tns:FetchStudentRecord">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="process">
<soap:operation soapAction="http://studentdatabank.org/process"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<service name="FetchStudentRecord">
<port name="FetchStudentRecordPort" binding="tns:FetchStudentRecordBinding">
<soap:address location="http://192.168.1.70:9766/services/FetchStudentRecord/"/>
</port>
</service>
<plnk:partnerLinkType name="OxfordLinkType">
<plnk:role name="OxfordRole" portType="wsdl:IAwardService"/>
</plnk:partnerLinkType>
<plnk:partnerLinkType name="FetchStudentRecord">
<plnk:role name="FetchStudentRecordProvider" portType="tns:FetchStudentRecord"/>
</plnk:partnerLinkType>
</definitions>
SOAP 请求正文示例
<p:FetchStudentRecordRequest xmlns:p="http://studentdatabank.org">
<universityCode xmlns="http://studentdatabank.org">?</universityCode>
<studentId xmlns="http://studentdatabank.org">?</studentId>
</p:FetchStudentRecordRequest>
7.3 REST API 的 SOAP 暴露
对于那些不能消费 REST API 而需要 SOAP 端点的客户端,我们实现了通用的 ESB SOAP 到 REST 代理。例如,这是一个 POST 代理,它接收 SOAP 请求并将其转换为 POX 并发布到 REST 服务,同时保留 URI 的资源标识符部分。
<proxy xmlns="http://ws.apache.org/ns/synapse" name="POSTSOAP2REST" transports="http" statistics="disable" trace="disable" startOnLoad="true">
<target>
<inSequence>
<property name="HTTP_METHOD" value="POST" scope="axis2"/>
<property xmlns:ns3="http://org.apache.synapse/xsd" name="Lang" expression="get-property('transport', 'Accept')" scope="default" type="STRING"/>
<property name="DISABLE_CHUNKING" value="true" scope="axis2" type="STRING"/>
<property name="REST_URL_POSTFIX" scope="axis2" action="remove"/>
<property name="messageType" value="application/xml" scope="axis2"/>
<property xmlns:m="http://studentdatabank.org/esb" name="REST_URL_POSTFIX" expression="//m:soap2rest/m:request/m:uri/text()" scope="axis2"/>
<xslt key="UniversitySoap2POXRequest"/>
<send>
<endpoint>
<address uri="https://:1657/api" format="rest"/>
</endpoint>
</send>
</inSequence>
<outSequence>
<log level="full"/>
<property name="messageType" value="application/xml" scope="axis2"/>
<send/>
</outSequence>
</target>
<description></description>
</proxy>
它接收如下请求
<m:soap2rest xmlns:m="http://studentdatabank.org/esb">
<m:request>
<m:uri>/Institutes</m:uri>
<m:method>POST</m:method>
<m:body>
<EducationalInstitute xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://studentdatabank.org">
<Address>New, UK</Address>
<Code>New</Code>
<Name>New University2</Name>
</EducationalInstitute>
</m:body>
</m:request>
</m:soap2rest>
<m:uri> 节点指定资源标识符是什么。例如,在此例中,它是整个 Institutions 集合。<m:method> 定义协议,<m:body> 定义实际 POST 到底层 REST API 的 POX 有效负载。
以下 XSL 用于将 SOAP 有效负载转换为 POX 有效负载
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml"/>
<xsl:template xmlns:m="http://studentdatabank.org/esb" match="/">
<xsl:apply-templates select="//m:body"/>
</xsl:template>
<xsl:template xmlns:m="http://studentdatabank.org/esb" match="m:body">
<xsl:value-of select="." disable-output-escaping="yes"/>
</xsl:template>
</xsl:stylesheet>
这个 xslt 提取并解码 <m:body> 中编码的 XML。然后将其 POST 到底层 REST API。
8 服务交互
8.1 方法
常见的服务交互模式是:网站通过 ESB 调用 REST API 和 BPS。BPS 通过 ESB 调用 REST API 和外部 EI 端点。EI 客户端系统可以通过 ESB 上的 REST API 和 BPS 进程的安全、有限的暴露来消费 REST API。
图 3 常见服务交互模式
8.2 网站与数据服务交互
网站通过 ESB 消费 REST 数据服务。ESB 在此仅充当代理,不应用任何安全性,只进行日志记录。以下是网站消费 REST 数据服务的一些场景
8.2.1 教育机构管理员主页
当教育机构的管理员用户登录时,用户会看到此视图
图 4 教育机构管理员用户视图
前端是一个 ASP.NET MVC 网站,它消费使用 ASP.NET MVC WebApi 框架实现的 REST 数据服务。交互方式如下
图 5 网站从 REST 数据服务获取数据
以下视图 (Views\EducationalInstitute\Index.cshtml) 渲染 EI 提供的课程、项目以及 SDB 中有数据可用的注册学生。
@model StudentService.Models.EIViewModel
@{
ViewBag.Title = "Educational Institute" + Model.EducationalInstitute.Name;
}
<div id="body">
<section class="featured">
<div class="content-wrapper">
<hgroup class="title">
<h1>@Model.EducationalInstitute.Name</h1>
</hgroup>
</div>
</section>
<section class="content-wrapper main-content clear-fix">
<h3>Courses Offered</h3>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Universal Course</th>
</tr>
</thead>
<tbody>
@foreach (var course in Model.Courses)
{
<tr>
<td>@course.Code</td>
<td>@course.Name</td>
<td>@course.UniversalCourseCode</td>
</tr>
}
</tbody>
</table>
<h3>Programs offered</h3>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Courses</th>
</tr>
</thead>
<tbody>
@foreach (var program in Model.Programs)
{
<tr>
<td>@program.Code</td>
<td>@program.Name</td>
<td>
@string.Join(", ", program.ProgramCourses.Select(pc => pc.Code).ToArray())
</td>
</tr>
}
</tbody>
</table>
<h3>Students registered</h3>
<table>
<thead>
<tr>
<th>Student ID</th>
<th>First name</th>
<th>Last name</th>
</tr>
</thead>
<tbody>
@foreach (var student in Model.Students)
{
<tr>
<td>@Html.RouteLink(student.StudentId, "EducationalInstituteStudent", new RouteValueDictionary( new {
action="Student",
universityCode = ViewContext.RouteData.Values["universityCode"],
studentId = student.StudentId }))</td>
<td>@student.Firstname</td>
<td>@student.Lastname</td>
</tr>
}
</tbody>
</table>
</section>
</div>
控制器 (Controllers\EducationalInstituteController.cs) 仅通过从数据库中收集必要数据来构建模型。
public class EducationalInstituteController : Controller
{
//
// GET: /institutes/{id}
private StudentServiceContext db = new StudentServiceContext();
public ActionResult Index(string universityCode)
{
return View(new EIViewModel
{
EducationalInstitute = db.EducationalInstitutes.First(u => u.Code == universityCode),
Courses = db.EducationalInstituteCourses.Where(uc => uc.EducationalInstitute.Code == universityCode),
Programs = db.Programs.Where(p => p.EducationalInstitute.Code == universityCode).Include("ProgramCourses").AsQueryable(),
Students = db.Students.Where(s => s.EducationalInstitute.Code == universityCode).Include("LinksToOtherEI")
});
}
Web API 需要一些特殊配置,特别是为了生成干净的 XML 输出。以下是 WebApiConfig
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "CoursesOfProgramsOfEducationalInstitute",
routeTemplate: "api/institutes/{universityCode}/programs/{programCode}/courses/{courseCode}",
defaults: new
{
controller = "ProgramCourse",
courseCode = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
name: "ProgramsOfEducationalInstitute",
routeTemplate: "api/institutes/{universityCode}/programs/{programCode}",
defaults: new {
controller = "Program",
programCode = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
name: "StudentOfEducationalInstitute",
routeTemplate: "api/institutes/{universityCode}/students/{studentId}",
defaults: new
{
controller = "Student",
studentId = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
name: "ProgramsOfStudentOfEducationalInstitute",
routeTemplate: "api/institutes/{universityCode}/students/{studentId}/programs/{programCode}",
defaults: new
{
controller = "StudentProgram",
programCode = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
name: "RefreshProgramOfStudentOfEducationalInstitute",
routeTemplate: "api/institutes/{universityCode}/students/{studentId}/programs/{programCode}/refresh",
defaults: new
{
controller = "StudentProgram",
action = "Refresh"
});
config.Routes.MapHttpRoute(
name: "CourseCreditedOfProgramOfStudentOfEducationalInstitute",
routeTemplate: "api/institutes/{universityCode}/students/{studentId}/programs/{programCode}/coursescredited/{courseCode}",
defaults: new
{
controller = "CourseCredited",
courseCode = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
name: "EducationalInstitutes",
routeTemplate: "api/institutes/{code}",
defaults: new
{
controller = "EducationalInstitute",
code = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
name: "UniversalCourses",
routeTemplate: "api/universal_courses/{code}",
defaults: new
{
controller = "UniversalCourse",
code = RouteParameter.Optional
});
//config.Formatters.XmlFormatter.UseXmlSerializer = true;
// Make XML default formatter
var xmlFormatter = config.Formatters.XmlFormatter;
xmlFormatter.Indent = true;
config.Formatters.Remove(xmlFormatter);
config.Formatters.Insert(0, xmlFormatter);
}
}
最后一部分是干净的 XML 输出生成。
对于 ASP.NET MVC 路由,这是路由配置
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"EducationalInstitute",
url: "Institutes/{universityCode}",
defaults: new { controller = "EducationalInstitute", action = "Index" }
);
routes.MapRoute(
"EducationalInstituteStudent",
url: "Institutes/{universityCode}/Students/{studentId}",
defaults: new { controller = "EducationalInstitute", action = "Student" }
);
routes.MapRoute(
"EducationalInstituteStudentProgram",
url: "Institutes/{universityCode}/Students/{studentId}/Programs/{programCode}",
defaults: new { controller = "EducationalInstitute", action = "StudentProgram" }
);
routes.MapRoute(
"RecalculateProgram",
url: "Institutes/{universityCode}/Students/{studentId}/Programs/{programCode}/recalculate",
defaults: new { controller = "EducationalInstitute", action = "RecalculateProgram" }
);
routes.MapRoute(
"ChangeCreditedCourse",
url: "Institutes/{universityCode}/Students/{studentId}/Programs/{programCode}/{creditedUniversityCode}/{creditedCourseCode}/{newStatus}",
defaults: new { controller = "EducationalInstitute", action = "ChangeCreditedCourse" }
);
routes.MapRoute(
"NewLink",
url: "Institutes/{universityCode}/Students/{studentId}/NewLink",
defaults: new { controller = "EducationalInstitute", action = "NewLink" }
);
routes.MapRoute(
"FetchCourses",
url: "Institutes/{universityCode}/Students/{studentId}/links/{otherUniversityCode}/{otherStudentId}/fetch",
defaults: new { controller = "EducationalInstitute", action = "FetchCourses" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
8.2.2 显示学生记录
学生仪表盘显示学生已完成的课程、已注册的课程以及课程状态,以及学生已建立联系的其他机构。
图 6 管理员用户查看学生档案
此视图的服务交互也很简单。所有数据都通过数据服务从数据库中获取。
8.2.3 确定学生是否满足了某个项目的所有要求
学生可以从多个 EI 完成课程。以下页面显示了学生从哪些 EI 完成了哪些课程,这些课程已计入某个项目
图 7 管理员用户查看哪些课程已计入项目
这里显示学生从两所大学完成的课程已被计入硕士学位。
有一个复杂的 REST API 来完成这个处理。首先向这个 URL 发送一个 POST 请求
c:\Java>curl https://:1657/api/institutes/OX/Students/OX123/Programs/MSC_SE/refresh -d "" -H "Content-Type: text/xml" -v
> POST /api/institutes/OX/Students/OX123/Programs/MSC_SE/refresh HTTP/1.1
> Content-Type: text/xml
> Content-Length: 0
>
< HTTP/1.1 202 Accepted
< Location: https://:1657/api/institutes/OX/students/OX123/programs/MSC_SE
< Content-Length: 0
它告诉客户端 API 已经启动了对学生从所有关联的 EI 完成的所有课程进行重新计算的异步处理,并检查有多少课程可以计入 MSC 项目。这个刷新过程只使用 SDB 中已有的数据。它不调用外部 EI 服务。这是 BPS 流程的工作。
一旦计算完项目要求,它就会显示哪些课程已接受,哪些仍在待处理。
图 8 管理员用户查看哪些课程已满足项目要求,哪些课程待处理
8.3 验证声称在其他机构完成课程的学生
EI 可以使用此表单向另一个 EI 请求学生是否属于该机构。请求通过网站提交,管理员用户填写表单以定义需要从哪个 EI 验证学生详细信息。根据此信息,流程将调用正确的 EI 端点以检查学生是否确实属于该机构。
请求被提交给一个名为“验证学生声明”的异步BPS 流程。该流程首先检查学生是否已获得 EI 的授权,并且授权记录是否已存储在 SDB 数据库中。如果未存储,则调用 EI 端点(如果存在),否则会为大学管理员排队一个手动操作以进行批准。
图 9 验证学生声明流程
8.4 从 EI 获取学生记录的工作流
管理员用户或学生可以从该屏幕启动从已验证的 EI 获取学生记录
点击后,网站异步调用获取学生记录异步流程,该流程调用 EI 端点以检索学生的记录。如果 EI 没有可用的端点,则会为 EI 管理员排队一个手动任务,以便管理员用户可以在网站上手动添加记录。
图 10 获取课程流程
9 安全性及其他非功能性方面
9.1 安全性
- 网站通过 HTTPS 暴露。使用标准基于角色的身份验证和授权,允许 EI 用户和学生登录 SDB 网站。
- REST API 和 BPS 流程都通过 ESB 在互联网上暴露。ESB 使用 X.509 客户端证书对客户端进行身份验证并加密通信通道。只有与 SDB 签订协议并同意不暴露学生私人记录后从 SDB 获取客户端证书的 EI 才能消费 SDB 服务。EI 使用证书的 DN 进行识别。这些策略存储在治理注册表中,并在 ESB 代理和端点定义中引用。使用证书而不是用户名、密码,首先是因为它是一个系统在进行身份验证,而不是用户;其次是为了避免将用户名和密码存储在客户端服务器上的某些配置文件中,这些文件可能会被盗。
- EI 可以通过 HTTPS 或使用特定的 X.509 客户端证书提供其服务。ESB 维护 EI 的端点,并使用治理注册表作为 WSDL、策略和证书的策略存储。
9.2 性能
9.2.1 缓存
REST API 暴露的一些数据很少更改。例如,EI 列表或 EI 提供的课程被频繁读取,但很少被修改。GET /Institutes/{InstituteCode} 和 GET /Institutes/{InstituteCode}/Courses 等 REST URL 由 ESB 缓存,以防止重复访问 REST 服务和底层数据库。
9.2.2 限流
由于 REST API 对 EI 开放,存在恶意大学客户端系统向 REST API 发送过多请求并耗尽可用线程的可能性。ESB 提供限流功能,将 REST API 的使用限制在一定范围内。
9.2.3 监控
ESB 的监控功能提供了一种方法,可以监控来自内部系统以及外部大学客户端的 REST 和 SOAP API 的使用情况。
10 SOA 设计与概念
10.1 数据服务为何选择 REST,而非 SOAP?
- REST 是一种出色的协议,可以抽象底层存储。它可以用一致的方式暴露标准关系数据库或任何云存储(如 Windows Azure 表存储)中的关系实体。
- REST 可以使用 JSON 暴露数据,这使得 JavaScript 客户端更容易、更快地消费。
10.2 为何在 REST 服务之上使用 SOAP 封装?
- REST 的一个不便之处在于,客户端必须直接使用 Http 库并处理 HTTP 状态码、使用流、处理编码、HTTP 头等。使用 SOAP 封装,客户端可以生成强类型客户端代理类,避免编写任何直接的 Http 操作代码。此外,客户端可以使用标准的 try…catch…finally 机制来处理异常。
- 大多数 SOAP 框架都提供检查、仪器化功能,这些功能可以通过 REST API 的 SOAP 封装直接使用。例如,WCF。
10.3 为何使用 ESB?为何不直接通过 HTTPS 暴露 API?
- REST API 在应用服务器上以纯 HTTP 方式托管,供内部消费。无需在应用服务器上处理复杂的安全策略设置。
- ESB 负责 API 的所有安全性、限流、请求/响应日志记录和监控。
- ESB 对 API 进行两种暴露:一种用于内部消费,不加密,直接绕过代理;另一种是加密并使用 X.509 客户端证书进行身份验证,限流,并对公众互联网进行广泛审计的暴露,供 EI 客户端消费。
- ESB 充当一层间接。如果 REST/SOAP API 需要移动或需要实现某些重大更改,可以修改 ESB 代理以进行快速简单的转换,并保持与现有客户端的向后兼容性。
10.4 为何使用治理注册表?
- 治理注册表作为 ESB 的策略存储和策略执行点 (PEP)。ESB 使用 GR 存储所有 WSDL、XSLT、静态 XML 片段。
- GR 是 ESB 和 BPS 的 WSDL、XSLT 和其他工件的共享存储。
- 如果不使用 GR,ESB 和 BPS 都将拥有重复的工件。
10.5 为何使用业务流程管理器?为何不将所有逻辑硬编码到 REST API 中?
- 通过代码更改编排逻辑成本很高,因为它需要重新部署。
- 编排逻辑可以使用流程图可视化,并由非程序员设计。
- 使用 BPS 可以非常容易地更改和版本化流程。
- BPS 可以运行长时间运行的异步流程,这在代码中很难实现,因为需要编写复杂的线程和状态管理逻辑。
10.6 为何没有 API 管理系统?
- SDB 仅向与 SDB 签订合同的 EI 提供 API。没有自助入驻流程允许 EI 注册并立即访问 API。
- 每个希望消费 API 的 EI 都需要生成一个特殊的 EI 特定客户端证书来对客户端进行身份验证。
- 为每个 EI 生成 X.509 客户端证书是手动的。同样,在 ESB 上安装 EI 的客户端证书并在 ESB 中创建 EI 端点也是手动的。因此,API 管理系统用处不大。
10.7 REST 数据服务的粒度
- REST 数据服务将单个实体和集合作为唯一的 REST 资源提供。
- 此解决方案中执行的常见旅程不需要大量的聚合,因此尚未提供聚合资源。
- 根据流行需求,可以提供 $expand 方法来加载子实体和父实体。
- 提供了一个复杂的 REST 资源,用于刷新学生已注册的课程状态。但它完全在 SDB 数据库内部工作。REST 仅用于操作 SDB 数据库中的数据。SOAP 用于复杂操作,特别是调用 BPS 流程。