浅析如何在Nancy中生成API文档
前言
前后端分離,或許是現(xiàn)如今最為流行開發(fā)方式,包括UWP、Android和IOS這樣的手機(jī)客戶端都是需要調(diào)用后臺(tái)的API來(lái)進(jìn)行數(shù)據(jù)的交互。
但是這樣對(duì)前端開發(fā)和APP開發(fā)就會(huì)面臨這樣一個(gè)問(wèn)題:如何知道每個(gè)API做什么?
可能,有人會(huì)在內(nèi)部形成一份word文檔、pdf;有人會(huì)建立一個(gè)單獨(dú)的站點(diǎn),然后將API的地址,參數(shù)等信息列在上面;有人會(huì)借助第三方的工具來(lái)生成一份文檔等。
當(dāng)然,這基本是取決于不同公司的規(guī)范。
說(shuō)起API文檔,就想到前段時(shí)間做的微信小程序,由于那個(gè)不完善的接口文檔,從而導(dǎo)致浪費(fèi)了很大一部分時(shí)間去詢問(wèn)接口相關(guān)的內(nèi)容(用的是老的接口)。
為了處理這個(gè)問(wèn)題,我認(rèn)為,如果能在寫某個(gè)API的時(shí)候就順帶將這個(gè)API的相關(guān)信息一并處理了是最好不過(guò)!
不過(guò)這并不是讓我們寫好一個(gè)接口后,再去打開word等工具去編輯一下這個(gè)API的信息,這樣明顯需要花費(fèi)更多的時(shí)間。
下面就針對(duì)這一問(wèn)題,探討一下在Nancy中的實(shí)現(xiàn)。
如何實(shí)現(xiàn)
其實(shí),想在Nancy中生成API文檔,是一件十分容易的事,因?yàn)樽髡?strong>thecodejunkie已經(jīng)幫我們?cè)贜ancy內(nèi)部提前做了一些處理
便于我們的后續(xù)擴(kuò)展,這點(diǎn)還是很貼心的。
下面我們先來(lái)寫點(diǎn)東西,后面才能寫相應(yīng)的API文檔。
public class ProductsModule : NancyModule{ ??public ProductsModule() : base("/products") ?
?{ ? ? ? ?Get("/", _ =>{ ? ? ? ? ?
??return Response.AsText("product list");}, null, "GetProductList"); ? ?
??? ?Get("/{productid}", _ =>{ ? ? ? ? ?
??? ??return Response.AsText(_.productid as string);}, null, "GetProductByProductId"); ? ? ?
??? ?? ?Post("/", _ =>{ ? ? ? ? ?
??? ?? ? ?return Response.AsText("Add product");}, null, "AddProduct"); ? ? ? ?//省略部分..} }
基本的CURD,沒有太多的必要去解釋這些內(nèi)容。當(dāng)然這里需要指出一點(diǎn)。
正常情況下,我們基本都是只寫前面兩個(gè)參數(shù)的,后面兩個(gè)參數(shù)是可選的。由于我們后面需要用到每個(gè)路由的名字
所以我們需要用到這里的第4個(gè)參數(shù)(當(dāng)前路由的名字),也就意味著我們要在定義的時(shí)候?qū)懚嘁稽c(diǎn)東西!
注: 1.x和2.x的寫法是有區(qū)別的!示例用的2.x的寫法,所以各位要注意這點(diǎn)!
以GET為例,方法定義大致如下
API寫好了,下面我們先來(lái)簡(jiǎn)單獲取一下這些api的相關(guān)信息!
最簡(jiǎn)單的實(shí)現(xiàn)
前面也提到,我們是要把這個(gè)api和api文檔放到同一個(gè)站點(diǎn)下面,免去編輯這一步驟!
世間萬(wàn)物都是相輔相成的,我們不想單獨(dú)編輯,自然就要在代碼里面多做一些處理!
新起一個(gè)Module名為DocModule,將api文檔的相關(guān)內(nèi)容放到這個(gè)module中來(lái)處理。
public class DocMudule : NancyModule{? ?private IRouteCacheProvider _routeCacheProvider; ?
? ?
? ? ?public DocMudule(IRouteCacheProvider routeCacheProvider) : base("/docs")
?{ ? ?
? ? ?? ?this._routeCacheProvider = routeCacheProvider; ? ? ?
? ? ?? ? ?Get("/", _ =>{ ? ? ? ? ? ?var routeDescriptionList = _routeCacheProvider.GetCache().SelectMany(x => x.Value).Select(x => x.Item2).Where(x => !string.IsNullOrWhiteSpace(x.Name)).ToList(); ? ? ? ? ?
? ? ?? ? ??return Response.AsJson(routeDescriptionList);});} }
沒錯(cuò),你沒看錯(cuò),就是這幾行代碼,就可以幫助我們?nèi)ド晌覀兿胍腶pi文檔!其實(shí)最主要的是IRouteCacheProvider這個(gè)接口。
它的具體實(shí)現(xiàn),會(huì)在后面的小節(jié)講到,現(xiàn)在先著重于使用!
先調(diào)用這個(gè)接口的GetCache方法,以拿到緩存的路由信息,這個(gè)路由信息有必要來(lái)看一下它的定義,因?yàn)椴豢此亩x,我們根本就沒有辦法繼續(xù)下去!
后續(xù)的查找都是依賴于這些緩存信息!
public interface IRouteCache : IDictionary<Type, List<Tuple<int, RouteDescription>>>,ICollection<KeyValuePair<Type, List<Tuple<int, RouteDescription>>>>,
?IEnumerable<KeyValuePair<Type, List<Tuple<int, RouteDescription>>>>, IEnumerable
{ ? ?bool IsEmpty(); }
看了上面的定義,就可以清楚的知道要用SelectMany去拿到那個(gè)元組的內(nèi)容。再取出元組的RouteDescription。
當(dāng)然,這個(gè)時(shí)候我們?nèi)〉降氖撬械穆酚尚畔?#xff0c;這些信息都包含了什么內(nèi)容呢?看看RouteDescription的定義就很清晰了。
public sealed class RouteDescription{ ? ?public RouteDescription(string name, string method, string path, Func<NancyContext, bool> condition); ? ?//The name of the routepublic string Name { get; set; } ? ?//The condition that has to be fulfilled inorder for the route to be a valid match.public Func<NancyContext, bool> Condition { get; } ? ?//The description of what the route is for.public string Description { get; set; } ? ?//Gets or sets the metadata information for a route.public RouteMetadata Metadata { get; set; } ? ?//Gets the method of the route.public string Method { get; } ? ?//Gets the path that the route will be invoked for.public string Path { get; } ? ?//Gets or set the segments, for the route, that was returned by the Nancy.Routing.IRouteSegmentExtractor.public IEnumerable<string> Segments { get; set; } }在查詢之后,我還過(guò)濾了那些名字為空的,不讓它們顯示出來(lái)。為什么不顯示出來(lái)呢?理由也比較簡(jiǎn)單,像DocModule,我們只定義了一個(gè)路由
而且這個(gè)路由在嚴(yán)格意義上并不屬于我們api的內(nèi)容,而且這個(gè)路由也是沒有定義名字的,所以顯示出來(lái)的意義也不大。
過(guò)濾之后,就得到了最終想要的信息!簡(jiǎn)單起見,這里是先直接 返回一個(gè)json對(duì)象,便于查看有什么內(nèi)容,便于在逐步完善后再把它結(jié)構(gòu)化。
下面是最簡(jiǎn)單實(shí)現(xiàn)后的大致效果:
在圖中,可以看到GetProductList和GetProductByProductId這兩個(gè)api的基本信息:請(qǐng)求的method,請(qǐng)求的路徑和路由片段。
但是這些信息真的是太少了!連api描述都見不到,拿出來(lái),肯定被人狠狠的罵一頓!!
下面我們要嘗試豐富一下我們的接口信息!
豐富一點(diǎn)的實(shí)現(xiàn)
要讓文檔充實(shí),總是需要一個(gè)切入點(diǎn),找到切入點(diǎn),事情就好辦了。仔細(xì)觀察上面的效果圖會(huì)發(fā)現(xiàn),里面的metadata是空的。當(dāng)然這個(gè)也就是豐富文檔內(nèi)容的切入點(diǎn)了。
從前面的定義可以看到,這個(gè)metadata是一個(gè)RouteMetadata的實(shí)例
public class RouteMetadata{ ? ?//Creates a new instance of the Nancy.Routing.RouteMetadata class.public RouteMetadata(IDictionary<Type, object> metadata); ?
?//Gets the raw metadata System.Collections.Generic.IDictionary`2.public IDictionary<Type, object> Raw { get; } ?
??//Gets a boolean that indicates if the specific type of metadata is stored.public bool Has<TMetadata>(); ? ?//Retrieves metadata of the provided type.public TMetadata Retrieve<TMetadata>(); }
這里對(duì)我們比較重要的是Raw這個(gè)屬性,因?yàn)檫@個(gè)是在返回結(jié)果中的一部分,它是一個(gè)字典,鍵是類型,值是這個(gè)類型對(duì)應(yīng)的實(shí)例。
先定義一個(gè)CustomRouteMetadata,用于返回路由的Metadata信息(可根據(jù)具體情況進(jìn)行相應(yīng)的定義)。這個(gè)CustomRouteMetadata就是上述字典Type。
public class CustomRouteMetadata{ ? ?// group by the modulepublic string Group { get; set; } ? ?// description of the apipublic string Description { get; set; } ? ?// path of the apipublic string Path { get; set; } ? ?// http method of the apipublic string Method { get; set; } ? ?// name of the apipublic string Name { get; set; } ? ?// segments of the apipublic IEnumerable<string> Segments { get; set; } }定義好我們要顯示的東西后,自然要把這些東西用起來(lái),才能體現(xiàn)它們的價(jià)值。
要用起來(lái)還涉及到一個(gè)MetadataModule,這個(gè)命名很像NancyModule,看上去都是一個(gè)Module。
先定義一個(gè)ProductsMetadataModule,讓它繼承MetadataModule<RouteMetadata>,
具體實(shí)現(xiàn)如下:
? ? ?typeof(CustomRouteMetadata), ? ? ? ? ? ? ?
? ? ?? ? ?new CustomRouteMetadata{Group = "Products",Description = "Get All Products from Database",Path = desc.Path,Method = desc.Method,Name = desc.Name,Segments = desc.Segments}}}; ? ? ? ? ? ?return new RouteMetadata(dic);};Describe["GetProductByProductId"] = desc =>{ ? ? ? ?
? ? ?? ? ?? ?var dic = new Dictionary<System.Type, object>{{ ? ? ? ? ? ? ? ? ?
? ? ?? ? ? ?typeof(CustomRouteMetadata), ? ? ? ? ? ?
? ? ?? ? ? ? ? ? ? ?new CustomRouteMetadata{Group = "Products",Description = "Get a Product by product id",Path = desc.Path,Method = desc.Method,Name = desc.Name,Segments = desc.Segments}}}; ? ? ? ? ? ?return new RouteMetadata(dic); ? ? ? ? ? ? ?}; ? ? ? ?//省略部分...} }
這里的寫法就和1.x里寫NancyModule的內(nèi)容是一樣的,應(yīng)該也是比較熟悉的。就不再累贅了。其中的desc是一個(gè)委托Func<RouteDescription, TMetadata>。
默認(rèn)返回的是一個(gè)RouteMetadata實(shí)例,而要?jiǎng)?chuàng)建一個(gè)這樣的實(shí)例還需要一個(gè)字典,所以大家能看到上面的代碼中定義了一個(gè)字典。
并且這個(gè)字典包含了我們自己定義的信息,其中Group和Description是完全的自定義,其他的是從RouteDescription中拿。
當(dāng)然,這里已經(jīng)開了一個(gè)口子,想怎么定義都是可以的!
完成上面的代碼之后,再來(lái)看看我們顯示的結(jié)果
可以看到我們添加的metadata相關(guān)的內(nèi)容已經(jīng)出來(lái)了!可能這個(gè)時(shí)候,大家也都發(fā)現(xiàn)了,似乎內(nèi)容有那么點(diǎn)重復(fù)的意思!
因?yàn)檫@些重復(fù),就會(huì)讓人感覺這里比較臃腫,所以我們肯定不需要取出太多重復(fù)的東西,目前只需要metadata下面的這些就可以了。
下面來(lái)對(duì)其進(jìn)行簡(jiǎn)化!
簡(jiǎn)化一點(diǎn)的實(shí)現(xiàn)
簡(jiǎn)化分為兩步:
第一步簡(jiǎn)化:DocModule的簡(jiǎn)化。
其實(shí),DocModule已經(jīng)是相當(dāng)?shù)暮?jiǎn)單了,但是還能在簡(jiǎn)潔一點(diǎn)點(diǎn)。這里用到了RetrieveMetadata這個(gè)擴(kuò)展方法來(lái)處理。
前面的做法是拿到路由的信息后,用了兩個(gè)Select來(lái)查詢,而且查詢出來(lái)的結(jié)果有那么一點(diǎn)臃腫,
而借助擴(kuò)展方法,可以只取metadata里面的內(nèi)容,也就是前面自定義的內(nèi)容,這才是我們真正意義上要用到的。
下面是具體實(shí)現(xiàn)的示例:
Get("/", _ =>{//01//var routeDescriptionList = _routeCacheProvider// ? ? ? ? ? ? ? ? ? ? ? ? ? ?.GetCache()// ? ? ? ? ? ? ? ? ? ? ? ? ? ?.SelectMany(x => x.Value)// ? ? ? ? ? ? ? ? ? ? ? ? ? ?.Select(x => x.Item2)// ? ? ? ? ? ? ? ? ? ? ? ? ? ?.Where(x => !string.IsNullOrWhiteSpace(x.Name))// ? ? ? ? ? ? ? ? ? ? ? ? ? ?.ToList();//return Response.AsJson(routeDescriptionList);//02var routeDescriptionList = _routeCacheProvider.GetCache().RetrieveMetadata<RouteMetadata>().Where(x => x != null);return Response.AsJson(routeDescriptionList);});經(jīng)過(guò)第一步簡(jiǎn)化后,已經(jīng)過(guò)濾了不少重復(fù)的信息了,效果如下:
第二步簡(jiǎn)化:Metadata的簡(jiǎn)化
在返回Metadata的時(shí)候,我們是返回了一個(gè)默認(rèn)的RouteMetadata對(duì)象,這個(gè)對(duì)象相比自定義的CustomRouteMetadata復(fù)雜了不少
而且從上面經(jīng)過(guò)第一步簡(jiǎn)化后的效果圖也可以發(fā)現(xiàn),只有value節(jié)點(diǎn)下面的內(nèi)容才是api文檔需要的內(nèi)容。
所以還要考慮用自定義的這個(gè)CustomRouteMetadata去代替原來(lái)的。
修改如下:
public class ProductsMetadataModule : MetadataModule<CustomRouteMetadata> { ? ?public ProductsMetadataModule() ? ?{Describe["GetProductList"] = desc =>{ ? ? ? ? ? ?return new CustomRouteMetadata{Group = "Products",Description = "Get All Products from Database",Path = desc.Path,Method = desc.Method,Name = desc.Name,Segments = desc.Segments};};Describe["GetProductByProductId"] = desc =>{ ? ? ? ? ? ?return new CustomRouteMetadata{Group = "Products",Description = "Get a Product by product id",Path = desc.Path,Method = desc.Method,Name = desc.Name,Segments = desc.Segments};}; ? ? ? ?//省略部分..} }由于MetadataModule<TMetadata> 中的TMetadata是自定義的CustomRouteMetadata,所以在返回的時(shí)候直接創(chuàng)建一個(gè)簡(jiǎn)單的實(shí)例即可
不需要像RouteMetadata那樣還要定義一個(gè)字典。
同時(shí),還要把DocModule中RetrieveMetadata的TMetadata也要替換成CustomRouteMetadata
var routeDescriptionList = _routeCacheProvider.GetCache() ? ? ? ? ? ? ? ? ? ? ? ? ? ?//.RetrieveMetadata<RouteMetadata>() ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?.RetrieveMetadata<CustomRouteMetadata>().Where(x => x != null);經(jīng)過(guò)這兩步的簡(jiǎn)化,現(xiàn)在得到的效果就是我們需要的結(jié)果了!
最后,當(dāng)然要專業(yè)一點(diǎn),不能讓人只看json吧!怎么都要添加一個(gè)html頁(yè)面,將這些信息展示出來(lái):
當(dāng)然,現(xiàn)在看上去還是很丑,文檔內(nèi)容也并不豐富,但是已經(jīng)把最簡(jiǎn)單的文檔做出來(lái)了,想要進(jìn)一步豐富它就可以自由發(fā)揮了。
實(shí)現(xiàn)探討
既然這樣簡(jiǎn)單的代碼就能幫助我們?nèi)ド蒩pi文檔,很有必要去研究一下Nancy幫我們做了什么事!
從最開始的IRouteCacheProvider入手,這個(gè)接口對(duì)應(yīng)的默認(rèn)實(shí)現(xiàn)DefaultRouteCacheProvider
public class DefaultRouteCacheProvider : IRouteCacheProvider, IDiagnosticsProvider{ ??/// <summary>/// The route cache factory/// </summary>protected readonly Func<IRouteCache> RouteCacheFactory; ? ?/// <summary>/// Initializes a new instance of the DefaultRouteCacheProvider class./// </summary>/// <param name="routeCacheFactory"></param>public DefaultRouteCacheProvider(Func<IRouteCache> routeCacheFactory) ?
?{ ? ? ? ?this.RouteCacheFactory = routeCacheFactory;} ? ?/// <summary>/// Gets an instance of the route cache./// </summary>/// <returns>An <see cref="IRouteCache"/> instance.</returns>public IRouteCache GetCache() ? ?{ ? ?
?? ?return this.RouteCacheFactory();} ? ?//省略部分..}
里面的GetCache方法是直接調(diào)用了定義的委托變量。最終是到了IRouteCache的實(shí)現(xiàn)類RouteCache,這個(gè)類算是一個(gè)重點(diǎn)觀察對(duì)象!
內(nèi)容有點(diǎn)多,就只貼出部分核心代碼了
它在構(gòu)造函數(shù)里去生成了路由的相關(guān)信息。
public RouteCache( ? ?INancyModuleCatalog moduleCatalog, ? ?INancyContextFactory contextFactory, ? ?IRouteSegmentExtractor routeSegmentExtractor, ? ?IRouteDescriptionProvider routeDescriptionProvider, ? ?ICultureService cultureService, ? ?IEnumerable<IRouteMetadataProvider> routeMetadataProviders){? ?this.routeSegmentExtractor = routeSegmentExtractor; ?
? ??this.routeDescriptionProvider = routeDescriptionProvider; ?
? ???this.routeMetadataProviders = routeMetadataProviders; ?
? ????var request = new Request("GET", "/", "http"); ?
? ?????using (var context = contextFactory.Create(request)){ ? ? ? ?this.BuildCache(moduleCatalog.GetAllModules(context));} }
具體的生成方法如下:遍歷所有的NancyModule,找到每個(gè)Module的RouteDescription集合(一個(gè)Module可以包含多個(gè)路由)
然后找到每個(gè)RouteDescription的描述,路由片段和metadata的信息。最后把這個(gè)Module路由信息添加到當(dāng)前的對(duì)象中!
private void BuildCache(IEnumerable<INancyModule> modules){ ? ?foreach (var module in modules){ ? ? ? ?var moduleType = module.GetType(); ? ?
? ?var routes =module.Routes.Select(r => r.Description).ToArray(); ? ?
? ? ? ?foreach (var routeDescription in routes){routeDescription.Description = this.routeDescriptionProvider.GetDescription(module, routeDescription.Path);routeDescription.Segments = this.routeSegmentExtractor.Extract(routeDescription.Path).ToArray();routeDescription.Metadata = this.GetRouteMetadata(module, routeDescription);} ? ? ? ?this.AddRoutesToCache(routes, moduleType);} }
前面提到RouteDescription的描述,路由片段和metadata的信息都是通過(guò)額外的方式拿到的,這里主要是拿metadata來(lái)做說(shuō)明
畢竟在上面最后的一個(gè)例子中,用到的是metadata的內(nèi)容。
先調(diào)用定義的私有方法GetRouteMetadata,這個(gè)方法里面的內(nèi)容是不是和前面的MetadataModule有點(diǎn)類似呢,字典和創(chuàng)建RouteMetadata的實(shí)例。
private RouteMetadata GetRouteMetadata(INancyModule module, RouteDescription routeDescription){ ? ?var data = new Dictionary<Type, object>(); ? ?foreach (var provider in this.routeMetadataProviders){ ? ? ? ?var type = provider.GetMetadataType(module, routeDescription); ? ? ? ?var metadata = provider.GetMetadata(module, routeDescription); ? ? ? ?if (type != null && metadata != null){data.Add(type, metadata);}} ? ?return new RouteMetadata(data); }重點(diǎn)的是provider。這個(gè)provider來(lái)源來(lái)IRouteMetadataProvider,這個(gè)接口就兩個(gè)方法。
Nancy這個(gè)項(xiàng)目中還有一個(gè)抽象類是繼承了這個(gè)接口的。但是這個(gè)抽象類是沒有默認(rèn)實(shí)現(xiàn)的。
public abstract class RouteMetadataProvider<TMetadata> : IRouteMetadataProvider{ ? ?public Type GetMetadataType(INancyModule module, RouteDescription routeDescription) ?
?{ ? ? ? ?return typeof(TMetadata);
?} ?
?public object GetMetadata(INancyModule module, RouteDescription routeDescription) ?
?{ ? ?
?? ?return this.GetRouteMetadata(module, routeDescription);} ?
?? ? ?protected abstract TMetadata GetRouteMetadata(INancyModule module, RouteDescription routeDescription); }
注:前面的原理分析都是基于Nancy這個(gè)項(xiàng)目。
這個(gè)時(shí)候,另外一個(gè)項(xiàng)目Nancy.Metadata.Modules就起作用了。我們編寫的MetadataModule也是要添加這個(gè)的引用才能正常使用的。
從上面編寫的MetadataModule可以看出這個(gè)項(xiàng)目的起點(diǎn)應(yīng)該是MetadataModule,而且有關(guān)metadata的核心也在這里了。
public abstract class MetadataModule<TMetadata> : IMetadataModule where TMetadata :class{ ?
?private readonly IDictionary<string, Func<RouteDescription, TMetadata>> metadata; ?
??protected MetadataModule() ?
?{ ? ?
??? ?this.metadata = new Dictionary<string, Func<RouteDescription, TMetadata>>();} ?
?// Gets <see cref="RouteMetadataBuilder"/> for describing routes.public RouteMetadataBuilder Describe{ ? ? ? ?get { return new RouteMetadataBuilder(this); }} ?
?// Returns metadata for the given RouteDescription.public object GetMetadata(RouteDescription description) ?
?{ ? ?
? ?if (this.metadata.ContainsKey(description.Name)){ ? ? ? ?
? ? ? ?return this.metadata[description.Name].Invoke(description);} ? ? ? ?return null;} ? ?// Helper class for configuring a route metadata handler in a module.public class RouteMetadataBuilder{ ? ? ? ?private readonly MetadataModule<TMetadata> parentModule; ?
? ? ? ?
? ? ? ?public RouteMetadataBuilder(MetadataModule<TMetadata> metadataModule) ? ?
? ?{ ? ? ? ? ? ?this.parentModule = metadataModule;} ?
? ? ? ?// Describes metadata for a route with the specified name.public Func<RouteDescription, TMetadata> this[string name]{ ? ? ? ?
? ? ? ?? ?set { this.AddRouteMetadata(name, value); }} ? ? ?
? ? ? ?? ?
? ??protected void AddRouteMetadata(string name, Func<RouteDescription, TMetadata> value) ? ? ? ?{ ? ? ? ? ? ?this.parentModule.metadata.Add(name, value);}} ? ?//省略部分..}
到這里,已經(jīng)將GetCache的內(nèi)內(nèi)外外都簡(jiǎn)單分析了一下。至于擴(kuò)展方法RetrieveMetadata就不在細(xì)說(shuō)了,只是selectmany和select的一層封裝。
寫在最后
本文粗略講解了如何在Nancy中生成API文檔,以及簡(jiǎn)單分析了其內(nèi)部的處理。
下一篇將繼續(xù)介紹這一塊的內(nèi)容,不過(guò)主角是Swagger。
原文地址:http://www.cnblogs.com/catcher1994/p/6791352.html
.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺(tái)或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔為你收集整理的浅析如何在Nancy中生成API文档的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: .NET的一点历史故事:擦肩而过的机遇
- 下一篇: Entity Framework Cor