Building microservices with ASP.NET Core (without MVC)(转)

There are several reasons why it makes sense to build super-lightweight HTTP services (or, despite all the baggage the word brings, “microservices”). I do not need to go into all the operational or architectural benefits of such approach to system development, as it has been discussed a lot elsewhere.

It feels natural that when building such HTTP services, it definitely makes sense to keep the footprint of the technology you chose as small as possible, not to mention the size of the codebase you should maintain long term.

In this point I wanted to show a couple of techniques for building very lightweight HTTP services on top ASP.NET Core, without the use of any framework, and with minimal code bloat.

Prerequisites

What I’ll be discussing in this article is based on ASP.NET Core 1.2 packages which at the time of writing have not shipped yet.

I am using the CI feed of ASP.NET Core, so my Nuget.config looks like this:

XHTML

1

2

3

4

5

6

<?xml version="1.0" encoding="utf-8"?>

<configuration>

<packageSources>

<add key="aspnetcidev" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />

</packageSources>

</configuration>

When version 1.2 ships to Nuget, this will no longer be required.

ASP.NET HTTP endpoints without MVC

ASP.NET Core allows you to define HTTP endpoints directly on top of the OWIN-like pipeline that it’s built around, rather than using the full-blown MVC framework and its controllers to handle incoming requests. This has been there since the very beginning – you could use middleware components to handle incoming HTTP requests and short circuit a response immediately to the client. A bunch of high profile ASP.NET Core based projects use technique like this already – for example Identity Server 4.

This is not a new concept – something similar existed (albeit in a limited fashion) in classic ASP.NET with HTTP modules and HTTP handlers. Later on, in Web API you could define message handlers for handling HTTP requests without needing to define controllers. Finally, in OWIN and in Project Katana, you could that by plugging in custom middleware components too.

Another alternative is to specify a custom IRouter and hang the various endpoints off it. The main difference between this approach, and plugging in custom middleware components is that routing itself is a single middleware. It also gives you a possibility for much more sophisticated URL pattern matching and route constraint definition – something you’d need handle manually in case of middlewares.

ASP.NET Core 1.2 will introduce a set of new extension methods on IRouter, which will make creation of lightweight HTTP endpoints even easier. It would be possible to also polyfill earlier versions of ASP.NET Core with this functionality by simply copying these new extensions into your project.

Setting up the base for a lightweight HTTP API

Here is the project.json for our microservice. It contains only the most basic packages.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

{

"dependencies": {

"Microsoft.NETCore.App": {

"version": "1.1.0",

"type": "platform"

},

"Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-preview1-23182",

"Microsoft.AspNetCore.Routing": "1.2.0-preview1-23182",

"Microsoft.AspNetCore.Server.Kestrel": "1.2.0-preview1-23182",

"Microsoft.Extensions.Configuration.Json": "1.2.0-preview1-23182",

"Microsoft.Extensions.Logging": "1.2.0-preview1-23182",

"Microsoft.Extensions.Logging.Console": "1.2.0-preview1-23182",

"Microsoft.Extensions.Options.ConfigurationExtensions": "1.2.0-preview1-23182"

},

"frameworks": {

"netcoreapp1.0": {

"imports": [

"dotnet5.6",

"portable-net45+win8"

]

}

},

"buildOptions": {

"emitEntryPoint": true,

"preserveCompilationContext": true

},

"runtimeOptions": {

"configProperties": {

"System.GC.Server": true

}

},

"publishOptions": {

"include": [

"wwwroot",

"appsettings.json",

"web.config"

]

},

"scripts": {

"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]

}

}

We are using the bare minimum here:

  • Kestrel and IIS integration to act as server host
  • routing package
  • logging and configuration packages

In order to keep our code to absolute minimum too, we can even ditch the Startup class concept for our API setup, and just do all of the backend API code in a single file. To do that, instead of hooking into the typical Startup extensibility points like Configure() and ConfigureServices() methods, we’ll hang everything off WebHostBuilder.

WebHostBuilder is quite often ignored/overlooked by ASP.NET Core developers, because it’s generated by the template as the entry point inside the Program class, and usually you don’t even need to modify it – as it by default points at Startup class where almost all of the set up and configuration work happens. However, it also exposes similar hooks that Startup has, so it is possible to just define everything on WebHostBuilder directly.

Our basic API configuration is shown below. It doesn’t do anything yet in terms of exposing HTTP endpoints, but it’s fully functional from the perspective of the pipeline set up (router is wired in), logging to console and consuming configuration from JSON and environment variables.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class Program

{

public static void Main(string[] args)

{

var config = new ConfigurationBuilder()

.SetBasePath(Directory.GetCurrentDirectory())

.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)

.AddEnvironmentVariables().Build();

var host = new WebHostBuilder()

.UseKestrel()

.UseConfiguration(config)

.UseContentRoot(Directory.GetCurrentDirectory())

.UseIISIntegration()

.ConfigureLogging(l => l.AddConsole(config.GetSection("Logging")))

.ConfigureServices(s => s.AddRouting())

.Configure(app =>

{

// to do - wire in our HTTP endpoints

})

.Build();

host.Run();

}

}

I love this approach because it’s so wonderfully concise. In roughly 20 lines of code we have an excellent base for a lightweight HTTP API. Naturally we could enrich this with more features as need – for example add in your own custom services or add token validation using the relevant integration packages from Microsoft.Security or IdetntityServer4.

Adding HTTP endpoints to our solution

The final step is to add our HTTP endpoints. We’ll do that using the aforementioned extension methods which will be introduced in ASP.NET Core 1.2. For demonstration purposes we’ll also need some sample model and emulated data, so I’ll be using my standard Contact and ContactRepository examples.

The code below goes into the Configure() extension method on the WebHostBuilder as it was noted before already. It shows the HTTP handlers for getting all contacts and getting a contact by ID.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

app.UseRouter(r =>

{

var contactRepo = new InMemoryContactRepository();

r.MapGet("contacts", async (request, response, routeData) =>

{

var contacts = await contactRepo.GetAll();

await response.WriteJson(contacts);

});

r.MapGet("contacts/{id:int}", async (request, response, routeData) =>

{

var contact = await contactRepo.Get(Convert.ToInt32(routeData.Values["id"]));

if (contact == null)

{

response.StatusCode = 404;

return;

}

await response.WriteJson(contact);

});

});

This code should be rather self descriptive – we delegate the look up operation to the backing repoistory, and then simply write out the result to the HTTP response. The router extension methods also gives us access to route data values, making it easy to handle complex URIs. On top of that, we can use regular ASP.NET Core route templates with all the power of the constraints which is very handy – for example, just like you’d expect, contacts/{id:int} will not be matched for non-integer IDs.

Additionally, I helped myself a bit by adding a convenience extension method to write to the response stream.

1

2

3

4

5

6

7

8

public static class HttpExtensions

{

public static Task WriteJson<T>(this HttpResponse response, T obj)

{

response.ContentType = "application/json";

return response.WriteAsync(JsonConvert.SerializeObject(obj));

}

}

The final step is to add in the HTTP endpoints that would modify the state on the server side:

  • POST a new contact
  • PUT a contact (modify existing)
  • DELETE a contact

We’ll need an extra extension method to simplify that – that is because have to deserialize the request body stream into JSON, and we’d also like to validate the data annotations on our model to ensure the request payload from the client is valid. Obviously it would be silly to repeat that code over and over.

This extension method is shown below, and it makes use of JSON.NET and the System.ComponentModel.DataAnnotations.Validator.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public static class HttpExtensions

{

private static readonly JsonSerializer Serializer = new JsonSerializer();

public static async Task<T> ReadFromJson<T>(this HttpContext httpContext)

{

using (var streamReader = new StreamReader(httpContext.Request.Body))

using (var jsonTextReader = new JsonTextReader(streamReader))

{

var obj = Serializer.Deserialize<T>(jsonTextReader);

var results = new List<ValidationResult>();

if (Validator.TryValidateObject(obj, new ValidationContext(obj), results))

{

return obj;

}

httpContext.Response.StatusCode = 400;

await httpContext.Response.WriteJson(results);

return default(T);

}

}

}

Notice that the method will short-circuit a 400 Bad Request response back to the client if the model is not valid (for example a required field was missing) – and it will pass the validation errors too.

The HTTP endpoint definitions are shown next.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

r.MapPost("contacts", async (request, response, routeData) =>

{

var newContact = await request.HttpContext.ReadFromJson<Contact>();

if (newContact == null) return;

await contactRepo.Add(newContact);

response.StatusCode = 201;

await response.WriteJson(newContact);

});

r.MapPut("contacts/{id:int}", async (request, response, routeData) =>

{

var updatedContact = await request.HttpContext.ReadFromJson<Contact>();

if (updatedContact == null) return;

updatedContact.ContactId = Convert.ToInt32(routeData.Values["id"]);

await contactRepo.Update(updatedContact);

response.StatusCode = 204;

});

r.MapDelete("contacts/{id:int}", async (request, response, routeData) =>

{

await contactRepo.Delete(Convert.ToInt32(routeData.Values["id"]));

response.StatusCode = 204;

});

And that’s it – you could improve this further by adding for example convenience methods of reading and casting values from RouteDataDictionary. It is also not difficult to enhance this code with authentication and even integrate the new ASP.NET Core authorization policies into it.

Our full “microservice” code (without the helper extension methods, I assume you’d want to centralize and reuse them anyway) is shown below – and I’m quite pleased with the result. I find it a very appealing, concise way of building lightweight APIs in ASP.NET Core.

The full source code is here on Github.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

public class Program

{

public static void Main(string[] args)

{

var config = new ConfigurationBuilder()

.SetBasePath(Directory.GetCurrentDirectory())

.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)

.AddEnvironmentVariables().Build();

var host = new WebHostBuilder()

.UseKestrel()

.UseConfiguration(config)

.UseContentRoot(Directory.GetCurrentDirectory())

.UseIISIntegration()

.ConfigureLogging(l => l.AddConsole(config.GetSection("Logging")))

.ConfigureServices(s => s.AddRouting())

.Configure(app =>

{

// define all API endpoints

app.UseRouter(r =>

{

var contactRepo = new InMemoryContactRepository();

r.MapGet("contacts", async (request, response, routeData) =>

{

var contacts = await contactRepo.GetAll();

await response.WriteJson(contacts);

});

r.MapGet("contacts/{id:int}", async (request, response, routeData) =>

{

var contact = await contactRepo.Get(Convert.ToInt32(routeData.Values["id"]));

if (contact == null)

{

response.StatusCode = 404;

return;

}

await response.WriteJson(contact);

});

r.MapPost("contacts", async (request, response, routeData) =>

{

var newContact = await request.HttpContext.ReadFromJson<Contact>();

if (newContact == null) return;

await contactRepo.Add(newContact);

response.StatusCode = 201;

await response.WriteJson(newContact);

});

r.MapPut("contacts/{id:int}", async (request, response, routeData) =>

{

var updatedContact = await request.HttpContext.ReadFromJson<Contact>();

if (updatedContact == null) return;

updatedContact.ContactId = Convert.ToInt32(routeData.Values["id"]);

await contactRepo.Update(updatedContact);

response.StatusCode = 204;

});

r.MapDelete("contacts/{id:int}", async (request, response, routeData) =>

{

await contactRepo.Delete(Convert.ToInt32(routeData.Values["id"]));

response.StatusCode = 204;

});

});

})

.Build();

host.Run();

}

}

原文:http://www.strathweb.com/2017/01/building-microservices-with-asp-net-core-without-mvc/

时间: 2024-10-24 11:58:12

Building microservices with ASP.NET Core (without MVC)(转)的相关文章

ASP.NET Core 配置 MVC - ASP.NET Core 基础教程 - 简单教程,简单编程

原文:ASP.NET Core 配置 MVC - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 配置 MVC 前面几章节中,我们都是基于 ASP.NET 空项目 模板创建的 HelloWorld 上做开发 通过这个最基本的 HelloWorld 项目,我们了解了很多知识,初窥了 ASP.NET Core,并对 ASP.NET Core 的运行机制有了一个基本的了解 MVC 模式是 Web 开发中最重要的一个模式之一,通过 MVC,我们可以将控制器.模型和视

ASP.NET Core开发-MVC 使用dotnet 命令创建Controller和View

使用dotnet 命令在ASP.NET Core MVC 中创建Controller和View,之前讲解过使用yo 来创建Controller和View. 下面来了解dotnet 命令来创建Controller和View,功能更加强大,更加完整. 结合VS Code 使你能跨平台更好更快速的开发 ASP.NET Core MVC. 也就可以在 Linux 和Mac 中更好的开发ASP.NET Core 应用程序. 创建ASP.NET Core应用程序 dotnet new -t web dotn

Asp.net Core基于MVC框架实现PostgreSQL操作

简单介绍 Asp.net Core最大的价值在于跨平台.跨平台.跨平台.重要的事情说三遍.但是目前毕竟是在开发初期,虽然推出了1.0.0 正式版,但是其实好多功能还没有完善.比方说编译时的一些文件编码问题,辅助工具Tools的一些Bug,还有一些好用的模板和平台实现尚未完成等一些问题.但这毕竟是一个好的开始,并且在Github上,大家都还在积极的完善,你也可以参与进来.地址:https://github.com/aspnet Asp.net Core在学习的时候,首先你应该跟着微软官方的入门教材

【ASP.NET Core】MVC中自定义视图的查找位置

.NET Core 的内容处处可见,刷爆全球各大社区,所以,老周相信各位大伙伴已经看得不少了,故而,老周不考虑一个个知识点地去写,那样会成为年度最大的屁话,何况官方文档也很详尽.老周主要扯一下大伙伴们在入门的时候可能会疑惑的内容. ASP.NET Core 可以在一个项目中混合使用 Web Pages 和 MVC ,这是老周最希望的,因为这样会变得更灵活.Web Pages 类似于我们过去的 Web 开发方式,以页面为单位,此模型侧重于功能划分.而 MVC 侧重于数据,有什么样的数据模型就有什么

通过 Docker Compose 组合 ASP NET Core 和 SQL Server

目录 Docker Compose 简介 安装 WebApi 项目 创建项目 编写Dockfile Web MVC 项目 创建项目 编写Dockfile 编写 docker-compose.yml文件 运行项目 源代码 参考 本文模拟一个比较完整的项目,包括前端(MVC), 后端(WebApi)和数据库(mssql-server-linux).通过Docker Compose 定义,组合并执行它们.涉及到 Docker Compose 安装,命令,docker-compose.yml文件编写,W

【目录】开始使用ASP.NET Core MVC和Visual Studio

参照微软教程:Building your first ASP.NET Core MVC app with Visual Studio This series of tutorials will teach you the basics of building an ASP.NET Core MVC web app using Visual Studio. Getting started Adding a controller Adding a view Adding a model Workin

【翻译】在Visual Studio中使用Asp.Net Core MVC创建你的第一个Web API应用(一)

HTTP is not just for serving up web pages. It's also a powerful platform for building APIs that expose services and data. HTTP is simple, flexible, and ubiquitous. Almost any platform that you can think of has an HTTP library, so HTTP services can re

ASP.NET Core MVC/WebAPi 模型绑定探索

前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用到了,你再去看理论性的文章时才会豁然开朗,这是我一直以来学习技术的方法.本文我们来讲解.NET Core中的模型绑定. 话题 在ASP.NET Core之前MVC和Web APi被分开,也就说其请求管道是独立的,而在ASP.NET Core中,WebAPi和MVC的请求管道被合并在一起,当我们建立控

Pro ASP.NET Core MVC 6th 第三章

第三章 MVC 模式,项目和约定 在深入了解ASP.NET Core MVC的细节之前,我想确保您熟悉MVC设计模式背后的思路以及将其转换为ASP.NET Core MVC项目的方式. 您可能已经了解本章中讨论的一些想法和约定,特别是如果您已经完成了高级ASP.NET或C#开发. 如果没有,我鼓励你仔细阅读 - 深入地理解隐藏在MVC背后的东西可以帮助你在通读本书时更好地与MVC框架的功能联系起来. MVC的历史 模型视图控制器模式起源于20世纪70年代后期,来自施乐PARC的Smalltalk