WebApi 2 路由机制

.net中包含的路由有两种,第一种是MVC模式的按url匹配action,第二种是WebApi模式的按http请求的方法匹配action,本文我们学习WebApi的路由模式。

类型 路由模式
MVC 按url匹配action
WebApi http请求的方法匹配action

1.路由基础

首先新建一个WebApi项目,选择ASP.NET Web应用程序(.NET Framework)

ASP.NET Web应用程序(.NET Framework)
这里我们不加入MVC

这里我们首先来看WebApi的基础配置WebApiConfig.cs,其路径为/App_Start/WebApiConfig.cs

WebApiConfig.cs

首先以浏览器启动webapi项目

会发现页面显示403,这是很正常的,因为我们在新建项目的时候没有加入MVC,所以没有可视化的view页面,不过我们却拿到了这个项目的端口号

为了更加方便直观的看我们的接口请求,我们选择postman,首先在项目新建一个controller

通过以上的操作,我们就搭建好了一个webApi2的项目工程,后面我们就在这个工程的基础上进行webApi 2的开发和学习。

2.路由过程

webApi的路由过程主要经历了如下三步

  1. 根据请求的url匹配路由模板

当 Web API 框架收到 HTTP 请求时,它会尝试将 URI 与路由表(MapHttpRoute)中的某个路由模板匹配。 如果没有路由匹配,则客户端将收到404错误

例如 我们的WebApiConfig的路由模板如下

// App_start/WebApiConfig.cs
routes.MapHttpRoute(
   name: "DefaultApi",
   routeTemplate: "api/{controller}/{id}",
   defaults: new { id = RouteParameter.Optional }
)

路由表(MapHttpRoute)的默认路由模板为API/{controller}/{id} 。 在此模板中, api 是文本路径段,{controller}{id}是占位符变量。

controller如下

namespace WebApi.Controllers
{
   public class StudentsController : ApiController
   {
       public string GetStudent(string stu)
       {
           return "有参数Get成功";
       }
       public string GetStudent()
       {
           return "无参数Get成功";
       }
   }
}

当 Web API 框架收到 HTTP 请求时,它会尝试将 URI 与路由表中的某个路由模板匹配。 如果没有路由匹配,则客户端将收到404错误。例如,以下 Uri 与默认路由匹配:

/API/StudEnts //无视大小写
/api/stuDents  //无视大小写
/api/students/1
/api/students/gizmo1
/contacts/1 //无法匹配,因为它缺少 " api " 字段
  1. 找到控制器

首先在路由字典中找到实际的控制器的名称(比如“student”),然后在此控制器名称上面加上字符串“Controller”的到请求控制器的全称(比如“studentController”),最后找到对应的WebApi的Controller,实例化就得到当前请求的控制器对象。

  1. 找到action

得到了控制器对象之后通过如下三点进行action的匹配

  • 解析当前的http请求,得到请求类型(是get、post、put还是delete),例如,对于 GET 请求,Web API 将查找以 get 作为前缀的操作 " " ,如 " GetStudents " 。 此约定仅适用于 GETPOSTPUTDELETEHEADOPTIONSPATCH 动词。 当然你也可以通过使用控制器上的特性来启用其他 HTTP类型的请求
  • 如果路由模板配置了{action},则直接取到url里面的action名称
  • 解析请求的参数

修改controller如下

public class StudentsController : ApiController
   {
       public string GetStudent(string stu)
       {
           return "有参数Get成功";
       }
       public string GetStudent()
       {
           return "无参数Get成功";
       }
       public string DeleteStudents() 
      {
          return "调用delet成功";
      }
      public string postStudents()
      {
          return "调用post成功";
      }      
  }

依次对以下地址进行请求

请求类型 URI 操作(action) 参数
GET api/Studentshttp://localhost:61035/api/students?asdsadsad= GetStudent
GET api/students/xxx(任意字符)api/students?asdsadsad=1 GetStudent(string stu) xxx(任意字符)
DELETE api/students DeleteStudents()
POST api/students postStudents()

3.WebApiConfig.cs

3.1 webApi启动流程

WebApiConfig.cs这个文件是进行路由表的核心文件,WebApiConfig里面只有一个方法,这个方法在Global.asax文件里的Application_Start()方法被调用,Global.asax文件是一个全局文件,当我们网页启动时就会去执行它。Register(HttpConfiguration config)方法是配置WEB API路由的。

// App_start/WebApiConfig.cs
public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 路由
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",//name在一个项目中不允许重复
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
// Global.asax
namespace WebApi
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);  //全局路由配置的启动
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
    }
}

因此 在webAP工程启动的时候,会执行Global.asax,这个文件里面注册了WebApiConfig.cs的一系列初始化配置,从而实现了webapi。

3.2 路由表(MapHttpRoute)

WebApi 2 框架使用路由表。 并由Web API 的 Visual Studio 项目模板创建默认路由

//  App_start/WebApiConfig.cs
config.routes.MapHttpRoute(
    name: "API Default",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

转到MapHttpRoute的定义,可以看到它有4个重载

分别来看看各个参数的作用

  • name:要映射的路由的名称,如果你有好几个路由配置,需要保证他们各自的name不相同
  • routeTemplate:路由的路由模板,api/{controller}/{id}表示路由的url规则,api是固定部分,主要用来标识当前请求的url是一个api服务的接口,区别MVC的路由,当然,这里并不是一定要写成api,如果你改成webapi,那么你请求的url里面也需要写成webapi{controller}是控制器的占位符部分,该部分对应的是具体的控制器的名称,{id}是参数的占位符部分,表示参数,一般这个参数都会在default里面设置可选。有了这个路由模板约束请求的url,比如:我们请求的url写成http://localhost:65066/api2,那么肯定是找不到对应的路由的,如果请求的url匹配不到对应的路由,则会向客户端返回一个404的状态码。
  • defaults:一个包含默认路由值的对象,对于routeTemplate中,{controller}{id}部分都可以设置默认值如new { controller="Order", id = RouteParameter.Optional },其中RouteParameter.Optional表示路由的默认值是一个可选参数,可传可不传
  • constraints:一组表达式,用于指定 routeTemplate 的值,表示路由约束,一般是一个约束路由模板的正则表达式。比如:我们加入约束条件constraints: new { id = @"\d+" } ,这就约束必须要匹配一到多个参数id。如
虽然id是可选,但是不传的话不满足/\d+/,所以404
abc不满足/\d+/,所以404

当然defaults也可以使用表达式去约束controlleraction,大家可以根据业务需要自行研究。

当然,WebApi也支持MVC里面的路由机制,但RestFul风格的服务要求请求的url里面不能包含action,所以,在WebApi里面是并不提倡使用MVC路由机制的。

4.默认路由

webApi2工程自动为我们创建了默认的路由

//WebApiConfig.cs
public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            //Web API 路由
            config.MapHttpAttributeRoutes();

            //1.默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
 }

经过上面的讲解,我们知道了匹配的URL是api/{controller}/{id}
那么我们在实际的请求中应该如何请求

新建一个StudentsController

public class StudentsController : ApiController
   {
       public string GetStudent(string stu)
       {
           return "有参数Get成功";
       }
       public string GetStudent()
       {
           return "无参数Get成功";
       }
       public string DeleteStudents() 
      {
          return "调用delet成功";
      }
      public string postStudents()
      {
          return "调用post成功";
      }      
  }

依次对以下地址进行请求

请求类型 URI 操作(action) 参数
GET api/Studentshttp://localhost:61035/api/students?asdsadsad= GetStudent
GET api/students/xxx(任意字符)api/students?asdsadsad=1 GetStudent(string stu) xxx(任意字符)
DELETE api/students DeleteStudents()
POST api/students postStudents()

总结:

  • 在默认路由模板中,进行URI匹配是按照 文本路径段/控制器/参数(api/{controller}/{id}) 来进行匹配的
  • 可以使用restful方式进行请求,请求的参数以/进行分割,具体可以见
  • 控制器属性以方法开头的命名为准,如以get、post等开头,才能被webapi所识别,否则会找不到对应的控制器属性
  • 控制器属性如果都是无参的方法,我们请求这个控制器的时候带上了参数,会出现以下两种情况

1.控制器的属性都是不同类型的请求,此时不会有影响

public class StudentsController : ApiController
   {
       public string GetStudent()
       {
           return "调用Get成功";
       }
      
       public string postStudents()
       {
           return "调用post成功";
       }
   }


2.控制器的属性都是不同类型的请求,此时会提示 找到了与该请求匹配的多个操作

public class StudentsController : ApiController
   {
       public string GetStudent()
       {
           return "调用Get成功";
       }
       public string GetStudent2()
       {
           return "调用Get2成功";
       } 
   }
  • 控制器属性如果都是有参的方法,我们请求这个控制器下对应请求类型的属性没有带上了参数,会出现以下两种情况
  1. 除了这个请求类型的控制器属性,没有其它的无参控制器属性(无论什么请求类型),直接返回404
public class StudentsController : ApiController
   {
       public string GetStudent(string name)
       {
           return "调用Get成功";
       }
       public string GetStudent2(string name)
       {
           return "调用Get2成功";
       }
       public string DeleteStudents(string name)
       {
           return "调用delet成功";
       }
       public string postStudents(string name)
       {
           return "调用post成功";
       }
   }
image.png

默认路由的缺点

  • 默认路由只能匹配到controller这一级别,具体的请求类型需要控制器的属性名称以get、post开头,限制了控制器的属性命名的灵活度
  • 虽然同名的控制器属性可以通过重载解决不同数量参数的响应问题,但是容易出现不同控制器属性名称定义了相同请求类型和请求参数的时,webapi会出现 找到了与该请求匹配的多个操作,如下所示。
//WebApiConfig.cs
public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();
            //1.默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
public class studentController : ApiController
    {
        public string postAddStudent(string id)
        {
            return "添加学生";
        }
        public string postUpdateStudent(string id)
        {
            return "更新学生";
        }
        public string postDeletStudent(string id)
        {
            return "删除学生";
        }
    }

如下

//WebApiConfig.cs
public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            //Web API 路由
            config.MapHttpAttributeRoutes();

            //1.默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
public class studentController : ApiController
    {
        [HttpGet]
        public string StudentName()
        {
            return "小明";
        }
        [HttpGet]
        public string teacherName(string id)
        {
            return "老师";
        }
    }
http://localhost:65066/api/student
http://localhost:65066/api/student/1

添加如下的方法(action)

得到了一下的结果

我们来看看这个请求是如何进行匹配的,首先找到/App_Start/WebApiConfig.cs

namespace WebApi学习
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务
            // Web API 路由
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                //  routeTemplate:路由的模板
                //  api/{controller}:必选参数
                //  {id}:可选参数
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

按照路由模板来看,我们请求的路由没有action名称,那么它是怎么进行匹配的呢?
其实当我们访问http://localhost:65066/api/student这个url的时候,webapi会自动去匹配api/{controller}/{id}这个模板,在这个路径中,student是controller,那么它又是怎么去找到getStudentName这个action的呢?明明我们都没有传action这个参数,其实Webapi的路由规则是通过http方法去匹配对应的action,我们请求这个地址是用的GET方法,那么webapi会找Order这个控制器里面的get请求的方法,同事我们的这个getStudentName是以get开头的,它符合了webapi的匹配规则,于是就请求成功了,但前提是你写的方法必须是以get开头的,如果当前这个controller一个get开头的都没有,那么就显示请求的资源不支持 http 方法“GET”

当然不以get开头的前提是你必须加上[HttpGet]这个特性,webapi才知道你这个是get的请求方法,就能正确进行匹配。

[HttpGet]特性

然后我们再回到/App_Start/WebApiConfig.cs,看下里面MapHttpRoute的各个参数的含义,首先转到它的定义,发现它是HttpRouteCollection的扩展方法,MapHttpRoute有4类重载

  • name:要映射的路由的名称,如果你有好几个路由配置,需要保证他们各自的name不相同
  • routeTemplate:路由的路由模板,api/{controller}/{id}表示路由的url规则,api是固定部分,主要用来标识当前请求的url是一个api服务的接口,区别MVC的路由,当然,这里并不是一定要写成api,如果你改成webapi,那么你请求的url里面也需要写成webapi{controller}是控制器的占位符部分,该部分对应的是具体的控制器的名称,{id}是参数的占位符部分,表示参数,一般这个参数都会在default里面设置可选。有了这个路由模板约束请求的url,比如:我们请求的url写成http://localhost:65066/api2,那么肯定是找不到对应的路由的,如果请求的url匹配不到对应的路由,则会向客户端返回一个404的状态码。
缺少参数类型 请求结果
api填错、缺少api、缺少controller、controller填错 404
在contriller里面没有找到你请求的方法 请求的资源不支持${请求类型}方法“GET”
  • defaults:一个包含默认路由值的对象,对于routeTemplate中,{controller}和{id}部分都可以设置默认值如new { controller="Order", id = RouteParameter.Optional },其中RouteParameter.Optional表示路由的默认值是一个可选参数,可传可不传
  • constraints:一组表达式,用于指定 routeTemplate 的值,表示路由约束,一般是一个约束路由模板的正则表达式。比如:我们加入约束条件constraints: new { id = @"\d+" } ,这就约束必须要匹配一到多个参数id。如
虽然id是可选,但是不传的话不满足/\d+/,所以404
abc不满足/\d+/,所以404

当然defaults也可以使用表达式去约束controlleraction,大家可以根据业务需要自行研究。

当然,WebApi也支持MVC里面的路由机制,但RestFul风格的服务要求请求的url里面不能包含action,所以,在WebApi里面是并不提倡使用MVC路由机制的。

2.自定义路由

2.1 action路由

WebApiConfig.cs里面修改为如下的路由模板

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace WebApi学习
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            // Web API 路由
            config.MapHttpAttributeRoutes();

            //1.默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //2.自定义路由一:匹配到action
            config.Routes.MapHttpRoute(
                name: "ActionApi",
                routeTemplate: "actionapi/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            //3.自定义路由二
            config.Routes.MapHttpRoute(
                name: "TestApi",
                routeTemplate: "testapi/{controller}/{ordertype}/{id}",
                defaults: new { ordertype = "aa", id = RouteParameter.Optional }
            );
            
        }
    }
}
public class studentController : ApiController
    {
        [HttpGet]
        public string StudentName()
        {
            return "小明";
        }
    }

接口请求如下

通过action的名称来匹配很好理解,上面的StudentName()是方法名,webApi会默认它就是action的名称,如果你想要方法名和action的名称不一致,你也可以自定义action的名称,这个可以通过特性ActionName来实现,如下:

public class StudentController : ApiController
    {
        [ActionName("peopleName")]
        [HttpGet]
        public string StudentName()
        {
            return "小明";
        }
    }

2.2 特性路由

首先看路由模板

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            // Web API 路由
            //config.MapHttpAttributeRoutes();

            ////1.默认路由
            //config.Routes.MapHttpRoute(
            //    name: "DefaultApi",
            //    routeTemplate: "api/{controller}/{id}",
            //    defaults: new { id = RouteParameter.Optional }
            //);

            ////2.自定义路由一:匹配到action
            //config.Routes.MapHttpRoute(
            //    name: "ActionApi",
            //    routeTemplate: "actionapi/{controller}/{action}/{id}",
            //    defaults: new { id = RouteParameter.Optional }
            //);

            //3.自定义路由二
            config.Routes.MapHttpRoute(
                name: "TestApi",
                routeTemplate: "testapi/{controller}/{type}/{id}",
                defaults: new { type = "aa", id = RouteParameter.Optional }
            );
        }
    }
这里会发现无论type写什么都得到api的响应
即使不写type也能请求到api

4.特性路由

对于同请求类型,同请求参数的请求,会出现不传参数,找不到匹配的资源传了参数,但是找到了与该请求匹配的多个操作的问题,如下所示

//WebApiConfig.cs
public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            //Web API 路由
            config.MapHttpAttributeRoutes();

            //1.默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
public class studentController : ApiController
    {
        [HttpPost]
        public string addStudent(string id)
        {
            return "添加学生";
        }
        [HttpPost]
        public string updateStudent(string id)
        {
            return "更新学生";
        }
        [HttpPost]
        public string deletStudent(string id)
        {
            return "删除学生";
        }
    }
不传参数,找不到匹配的资源
找到了与该请求匹配的多个操作

解决以上的办法有两种

  • 通过特性ActionName路由即方法名不同实现
  • 通过特性路由实现

4.1 启用特性路由

如果要使用特性路由,首先在WebApiConfig.csRegister方法里面必须先启用特性路由(一般情况下,当我们新建一个WebApi项目的时候,会自动在Register方法里面加上这句话。)

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            //qi特性路由
            config.MapHttpAttributeRoutes();

            //1.默认路由
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }

特性路由的目的是为了解决我们公共路由模板引擎解决不了的问题。一个action定义了特性路由之后,就能通过特性路由上面的路由规则找到。
只要出现了特性路由,匹配的规则是按特性路由来的
修改studentController如下

 public class studentController : ApiController
    {
        [Route("teacher/addTeacher")]
        [HttpGet]
        public string addStudent(string id)
        {
            return "添加学生";
        }
        [Route("dog/adDog")]
        [HttpGet]
        public string updateStudent(string id)
        {
            return "更新学生";
        }
        [Route("cat/deletCat")]
        [HttpGet]
        public string deletStudent(string id)
        {
            return "删除学生";
        }
    }
public class StudentController : ApiController
    {
        [Route("")]
        [HttpGet]
        public string StudentName()
        {
            return "同学";
        }

        [Route("getTeacherName")]
        [HttpGet]
        public string TeacherName(string codeNumber)
        {
            return "老师";
        }
    }

4.2 带参数的特性路由

参考

4.3 带参数的特性路由(参数的约束和默认值)

参考

4.4 路由前缀

路由前缀的一般的做法是在控制器上面使用特性[RoutePrefix]来标识。
但是需要注意路由前缀不能以/开头

[RoutePrefix("apiPreFix/getName")]
    public class StudentController : ApiController
    {
        [Route("")]
        [HttpGet]
        public string StudentName()
        {
            return "同学";
        }

        [Route("getTeacherName")]
        [HttpGet]
        public string TeacherName(string codeNumber)
        {
            return "老师";
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容