การใช้งาน HATEOAS สำหรับโครงการ ASPNET Core Web API ซึ่งให้การควบคุมอย่างเต็มที่ว่าลิงก์ใดที่จะนำไปใช้กับโมเดลที่ส่งคืนจาก API ของคุณ เพื่อสื่อสารสถานะที่แตกต่างกันไปยังผู้ใช้ปลายทางห้องสมุดนี้รวมเข้ากับการอนุญาตอย่างเต็มที่และอนุญาตให้เงื่อนไขโดยพลการเพื่อตรวจสอบว่าจะแสดงหรือซ่อนการเชื่อมโยงความเกลียดชังระหว่างทรัพยากร API
ติดตั้งแพ็คเกจจาก nuget.org
PM > Install-Package RiskFirst.Hateoasซึ่งจะรวมถึงความเสี่ยงต่อการพึ่งพา first.hateoas.models ซึ่งแนะนำในเวอร์ชัน 3.0.0 เพื่อลบการพึ่งพา Aspnetcore จากชุดประกอบที่อ้างอิงคลาสฐาน LinkContainer
กำหนดค่าลิงค์เพื่อรวมสำหรับแต่ละรุ่นของคุณ
public class Startup
{
public void ConfigureServices ( IServicesCollection services )
{
services . AddLinks ( config =>
{
config . AddPolicy < MyModel > ( policy => {
policy . RequireSelfLink ( )
. RequireRoutedLink ( "all" , "GetAllModelsRoute" )
. RequireRoutedLink ( "delete" , "DeleteModelRoute" , x => new { id = x . Id } ) ;
} ) ;
} ) ;
}
} ฉีด ILinksService ลงในคอนโทรลเลอร์ใด ๆ (หรือคลาสอื่น ๆ ในโครงการของคุณ) เพื่อเพิ่มลิงก์ไปยังรุ่น
[ Route ( "api/[controller]" ) ]
public class MyController : Controller
{
private readonly ILinksService linksService ;
public MyController ( ILinksService linksService )
{
this . linksService = linksService ;
}
[ HttpGet ( "{id}" , Name = "GetModelRoute" ) ]
public async Task < MyModel > GetMyModel ( int id )
{
var model = await myRepository . GetMyModel ( id ) ;
await linksService . AddLinksAsync ( model ) ;
return model ;
}
[ HttpGet ( Name = "GetAllModelsRoute" ) ]
public async Task < IEnumerable < MyModel > > GetAllModels ( )
{
//... snip .. //
}
[ HttpDelete ( "{id}" , Name = "DeleteModelRoute" ) ]
public async Task < MyModel > DeleteMyModel ( int id )
{
//... snip .. //
}
}รหัสข้างต้นจะสร้างการตอบกลับเป็นตัวอย่างด้านล่าง
{
"id" : 1 ,
"someOtherField" : " foo " ,
"_links" : {
"self" : {
"rel" : " MyController \ GetModelRoute " ,
"href" : " https://api.example.com/my/1 " ,
"method" : " GET "
},
"all" : {
"rel" : " MyController \ GetAllModelsRoute " ,
"href" : " https://api.example.com/my " ,
"method" : " GET "
},
"delete" : {
"rel" : " MyController \ DeleteModelRoute " ,
"href" : " https://api.example.com/my/1 " ,
"method" : " DELETE "
}
}
}หรือถ้าคุณใช้ XML
<? xml version = " 1.0 " ?>
< MyModel xmlns : xsi = " http://www.w3.org/2001/XMLSchema-instance " xmlns : xsd = " http://www.w3.org/2001/XMLSchema " >
< link href = " https://api.example.com/my/1 " method = " GET " rel = " self " />
< link href = " https://api.example.com/my " method = " GET " rel = " all " />
< link href = " https://api.example.com/my/1 " method = " DELETE " rel = " delete " />
< Id >1</ Id >
< SomeOtherField >foo</ SomeOtherField >
</ MyModel > เป็นไปได้ที่จะระบุนโยบายที่มีชื่อหลายรายการสำหรับแบบจำลองในระหว่างการเริ่มต้นโดยการตั้งชื่อนโยบายให้กับ AddPolicy ตัวอย่างเช่นคุณอาจมีนโยบายเริ่มต้น (ไม่มีชื่อ) ให้ลิงก์พื้นฐานเมื่อโมเดลเป็นส่วนหนึ่งของรายการ แต่ข้อมูลรายละเอียดเพิ่มเติมเมื่อโมเดลถูกส่งคืนเพียงอย่างเดียว
public class Startup
{
public void ConfigureServices ( IServicesCollection services )
{
services . AddLinks ( config =>
{
config . AddPolicy < MyModel > ( policy => {
policy . RequireRoutedLink ( "self" , "GetModelRoute" , x => new { id = x . Id } )
} ) ;
config . AddPolicy < MyModel > ( "FullInfo" , policy => {
policy . RequireSelfLink ( )
. RequireRoutedLink ( "all" , "GetAllModelsRoute" )
. RequireRoutedLink ( "parentModels" , "GetParentModelRoute" , x => new { parentId = x . ParentId } ) ;
. RequireRoutedLink ( "subModels" , "GetSubModelsRoute" , x => new { id = x . Id } ) ;
. RequireRoutedLink ( "delete" , "DeleteModelRoute" , x => new { id = x . Id } ) ;
} ) ;
} ) ;
}
} ด้วยนโยบายที่มีชื่อสิ่งนี้สามารถนำไปใช้ในการรันไทม์โดยใช้ overload ของ AddLinksAsync ซึ่งใช้ชื่อนโยบาย:
await linksService . AddLinksAsync ( model , "FullInfo" ) ; นอกจากนี้คุณยังสามารถมาร์กอัปเมธอดคอนโทรลเลอร์ของคุณด้วย LinksAttribute เพื่อแทนที่นโยบายเริ่มต้นที่ใช้ รหัสด้านล่างจะใช้โปรไฟล์ "FullInfo" กับโมเดลที่ส่งคืนโดยไม่ต้องระบุชื่อนโยบายในการโทรไปยัง AddLinksAsync
[ Route ( "api/[controller]" ) ]
public class MyController : Controller
{
private readonly ILinksService linksService ;
public MyController ( ILinksService linksService )
{
this . linksService = linksService ;
}
[ HttpGet ( "{id}" , Name = "GetModelRoute" ) ]
[ Links ( Policy = "FullInfo" ) ]
public async Task < MyModel > GetMyModel ( int id )
{
var model = await myRepository . GetMyModel ( id ) ;
await linksService . AddLinksAsync ( model ) ;
return model ;
}
} อีกวิธีหนึ่งในการบรรลุสิ่งเดียวกันคือการทำเครื่องหมายวัตถุจริงด้วย LinksAttribute :
[ Links ( Policy = "FullInfo" ) ]
public class MyModel : LinkContainer
{ }
[ Route ( "api/[controller]" ) ]
public class MyController : Controller
{
private readonly ILinksService linksService ;
public MyController ( ILinksService linksService )
{
this . linksService = linksService ;
}
[ HttpGet ( "{id}" , Name = "GetModelRoute" ) ]
public async Task < MyModel > GetMyModel ( int id )
{
MyModel model = await myRepository . GetMyModel ( id ) ;
await linksService . AddLinksAsync ( model ) ;
return model ;
}
} มี AddLinksAsync มากเกินไปซึ่งใช้อินสแตนซ์ของ ILinksPolicy หรืออาร์เรย์ของ ILinksRequirement ซึ่งจะได้รับการประเมินเมื่อรันไทม์ สิ่งนี้ควรให้การควบคุมที่สมบูรณ์ว่าลิงก์ใดที่ใช้ ณ จุดใด ๆ ภายในรหัส API ของคุณ
ไม่ควรมีความจำเป็นอย่างยิ่งที่จะต้องเปลี่ยนวิธีการเปลี่ยนแปลง Href อย่างไรก็ตามข้อกำหนดทั่วไปหนึ่งข้อคือการส่งออกสัมพัทธ์แทนที่จะเป็น URIS สัมบูรณ์ สามารถลองได้ในตัวอย่างพื้นฐาน
services . AddLinks ( config =>
{
config . UseRelativeHrefs ( ) ;
.. .
} ) ; ทั้งการแปลง HREF และ REL สามารถควบคุมได้อย่างสมบูรณ์โดยการจัดหาคลาสหรือประเภทที่ใช้ ILinkTransformation
services . AddLinks ( config =>
{
// supply a type implementing ILinkTransformation
config . UseHrefTransformation < MyHrefTransformation > ( ) ;
// or supply an instance
config . UseRelTransformation ( new MyRelTransformation ( ) ) ;
} ) ;อีกทางเลือกหนึ่งการแปลงสามารถกำหนดค่าได้โดยใช้ไวยากรณ์ของตัวสร้าง
services . AddLinks ( config =>
{
// output a uri for the rel values
config . ConfigureRelTransformation ( transform => transform . AddProtocol ( )
. AddHost ( )
. AddVirtualPath ( ctx => $ "/rel/ { ctx . LinkSpec . ControllerName } / { ctx . LinkSpec . RouteName } " ) ;
} ) ;ทั้งสองวิธีของการแปลงรูปแบบการปรับแต่งสามารถเห็นได้ใน LinkConfigurationsample
เป็นไปได้ว่าคุณต้องการควบคุมลิงก์ใดที่รวมอยู่ในแต่ละรุ่นและข้อกำหนดทั่วไปหนึ่งข้อคือการแสดงลิงก์ที่ผู้ใช้ปัจจุบันได้รับอนุญาต ห้องสมุดนี้รวมเข้ากับไปป์ไลน์การอนุญาตอย่างเต็มที่และจะใช้นโยบายการอนุญาตใด ๆ ที่คุณใช้กับการดำเนินการที่เชื่อมโยง
เพื่อเปิดใช้งานการอนุญาตบนลิงค์ให้เงื่อนไข AuthorizeRoute
public class Startup
{
public void ConfigureServices ( IServicesCollection services )
{
services . AddLinks ( config =>
{
config . AddPolicy < MyModel > ( "FullInfo" , policy => {
policy . RequireSelfLink ( )
. RequireRoutedLink ( "all" , "GetAllModelsRoute" )
. RequireRoutedLink ( "parentModels" , "GetParentModelRoute" ,
x => new { parentId = x . ParentId } , condition => condition . AuthorizeRoute ( ) ) ;
. RequireRoutedLink ( "subModels" , "GetSubModelsRoute" ,
x => new { id = x . Id } , condition => condition . AuthorizeRoute ( ) ) ;
. RequireRoutedLink ( "delete" , "DeleteModelRoute" ,
x => new { id = x . Id } , condition => condition . AuthorizeRoute ( ) ) ;
} ) ;
} ) ;
}
} ในตัวอย่างข้างต้น GetParentModelRoute , GetSubModelsRoute & DeleteModelRoute จะไม่แสดงต่อผู้ใช้ที่ไม่สามารถเข้าถึงเส้นทางเหล่านั้นตามที่กำหนดโดยนโยบายการอนุญาตของพวกเขา ดูเอกสาร Microsoft สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการรับรองความถูกต้องภายในโครงการ Aspnet Core Webapi
เช่นเดียวกับตัวอย่างข้างต้นมีวิธีการเงื่อนไขเพิ่มเติมที่ช่วยให้คุณสามารถระบุชื่อนโยบายได้นโยบายที่สมบูรณ์หรือชุดของข้อกำหนด
นอกจากนี้คุณยังสามารถแสดงลิงก์ตามเงื่อนไขตามตรรกะบูลีนใด ๆ โดยใช้เงื่อนไข Assert ตัวอย่างเช่นมีวิธีการที่ช่วยให้คุณเพิ่มลิงก์เพจการเพจทั่วไปไปยังผลลัพธ์เพจของวัตถุ คุณอาจตัดสินใจได้ว่าสิ่งเหล่านี้ไม่คุ้มค่าหากมีผลทั้งหมดเพียงหนึ่งหน้า
options . AddPolicy < IPageLinkContainer > ( policy =>
{
policy . RequireelfLink ( "all" )
. RequirePagingLinks ( condition => condition . Assert ( x => x . PageCount > 1 ) ) ;
} ) ; คุณมีอิสระที่จะเพิ่มข้อกำหนดของคุณเองโดยใช้วิธีการทั่วไป Requires ใน LinksPolicyBuilder นอกจากนี้คุณต้องเขียนการใช้งานของ ILinksHandler เพื่อจัดการกับข้อกำหนดของคุณ ตัวอย่างเช่นคุณอาจมีข้อกำหนดเกี่ยวกับการตอบสนองบางอย่างเพื่อให้ลิงก์กลับไปยังเอกสารรูท API ของคุณ กำหนดข้อกำหนดง่าย ๆ สำหรับลิงค์นี้
using RiskFirst . Hateoas ;
public class ApiRootLinkRequirement : ILinksRequirement
{
public ApiRootLinkRequirement ( )
{
}
public string Id { get ; set ; } = "root" ;
} เมื่อพิจารณาจากข้อกำหนดนี้เราจำเป็นต้องมีคลาสในการจัดการซึ่งจะต้องใช้ ILinkHandler และจัดการกับข้อกำหนดของคุณ
using RiskFirst . Hateoas ;
public class ApiRootLinkHandler : LinksHandler < ApiRootLinkRequirement >
{
protected override Task HandleRequirementAsync ( LinksHandlerContext context , ApiRootLinkRequirement requirement )
{
var route = context . RouteMap . GetRoute ( "ApiRoot" ) ; // Assumes your API has a named route "ApiRoot".
context . Links . Add ( new LinkSpec ( requirement . Id , route ) ) ;
context . Handled ( requirement ) ;
return Task . CompletedTask ;
}
} ในที่สุดก็ลงทะเบียนตัวจัดการของคุณด้วย IServicesCollection และใช้ข้อกำหนดภายในนโยบายลิงก์ของคุณ
public class Startup
{
public void ConfigureServices ( IServicesCollection services )
{
services . AddLinks ( config =>
{
config . AddPolicy < MyModel > ( policy =>
{
policy . RequireRoutedLink ( "self" , "GetModelRoute" , x => new { id = x . Id } )
. Requires < ApiRootLinkRequirement > ( ) ;
} ) ;
} ) ;
services . AddTransient < ILinksHandler , ApiRootLinkHandler > ( ) ;
}
} ตัวอย่างนี้แสดงให้เห็นใน CustomRequirementSample
มีหลายส่วนของเฟรมเวิร์กที่สามารถขยายได้โดยการเขียนการใช้งานอินเทอร์เฟซที่เหมาะสมของคุณเองและลงทะเบียนด้วย IServicesCollection สำหรับการฉีดพึ่งพา ตัวอย่างเช่นคุณสามารถเปลี่ยนวิธีการประเมินลิงก์และนำไปใช้กับคอนเทนเนอร์ลิงค์ของคุณได้โดยใช้ ILinksEvaluator ของคุณเอง
using RiskFirst . Hateoas ;
public class Startup
{
public void ConfigureServices ( IServicesCollection services )
{
services . AddLinks ( options => {
.. .
} ) ;
services . AddTransient < ILinksEvaluator , MyLinksEvaluator > ( ) ;
}
}รายการอินเทอร์เฟซที่มีการใช้งานเริ่มต้น แต่สามารถแทนที่ได้คือ:
ILinkAuthorizationService ควบคุมวิธีการที่ลิงก์ได้รับอนุญาตในระหว่างการประเมินเงื่อนไขการเชื่อมโยงILinksEvaluator ควบคุมวิธีการประเมินและเปลี่ยนลิงก์ก่อนที่จะถูกเขียนไปยังโมเดลที่ส่งคืนILinksHandlerContextFactory ควบคุมวิธีการสร้างบริบทซึ่งผ่านตัวจัดการข้อกำหนดระหว่างการประมวลผลILinksPolicyProvider ให้การค้นหา ILinkPolicy ตามประเภทและชื่อทรัพยากรILinksService จุดเข้าใช้งานหลักในเฟรมเวิร์กอินเทอร์เฟซนี้จะถูกฉีดลงในรหัสผู้ใช้เพื่อใช้ลิงก์กับทรัพยากร APIILinkTransformationContextFactory ควบคุมว่าบริบทการแปลงถูกสร้างขึ้นระหว่างการแปลงสำหรับการเชื่อมโยง Rel & HrefIRouteMap ควบคุมวิธีการจัดทำดัชนี API ของคุณเพื่ออนุญาตลิงก์ระหว่างเส้นทาง การเปลี่ยนแปลงจากเวอร์ชัน 1.0.x เป็น 1.1.x ส่วนใหญ่ไม่ได้ทำลายอย่างไรก็ตามหากคุณได้ใช้ตัวจัดการข้อกำหนดที่กำหนดเองตามที่อธิบายไว้ในตัวอย่างด้านบนลายเซ็นของคลาสฐาน LinksHandler เปลี่ยนไปเล็กน้อยเพื่อลบการประกาศซ้ำซ้อนของประเภท TResource ทั่วไป
ใน v1.0.x รหัสของคุณอาจดูเหมือน:
public class MyCustomHandler : ILinksHandler { .. . } ตอนนี้ควรได้รับการสืบทอดจาก LinksHandler<TRequirement> การทำให้การใช้งานง่ายขึ้นและให้การแทนที่ประเภทของ HandleRequirementAsync เพื่อให้สามารถเข้าถึงความต้องการประเภทของคุณได้
public class MyCustomHandler : LinksHandler < MyCustomRequirement >