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:
|
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/
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 空项目 ...
- 基于ASP.NET core的MVC站点开发笔记 0x01
基于ASP.NET core的MVC站点开发笔记 0x01 我的环境 OS type:mac Software:vscode Dotnet core version:2.0/3.1 dotnet sd ...
- .NET Core RC2发布在即,我们试着用记事本编写一个ASP.NET Core RC2 MVC程序
在.NET Core 1.0.0 RC2即将正式发布之际,我也应应景,针对RC2 Preview版本编写一个史上最简单的MVC应用.由于VS 2015目前尚不支持,VS Code的智能感知尚欠火候,所 ...
- Asp.net Core基于MVC框架实现PostgreSQL操作
简单介绍 Asp.net Core最大的价值在于跨平台.跨平台.跨平台.重要的事情说三遍.但是目前毕竟是在开发初期,虽然推出了1.0.0 正式版,但是其实好多功能还没有完善.比方说编译时的一些文件编码 ...
- ASP.NET Core开发-MVC 使用dotnet 命令创建Controller和View
使用dotnet 命令在ASP.NET Core MVC 中创建Controller和View,之前讲解过使用yo 来创建Controller和View. 下面来了解dotnet 命令来创建Contr ...
- asp.net core 编译mvc,routing,security源代码进行本地调试
因为各种原因,需要查看asp.net core mvc的源代码来理解运行机制等等,虽说源代码查看已经能很好的理解了.但是能够直接调试还是最直观的.所有就有了本次尝试. 因调试设置源代码调试太辍笔,所以 ...
- 【ASP.NET Core】MVC中自定义视图的查找位置
.NET Core 的内容处处可见,刷爆全球各大社区,所以,老周相信各位大伙伴已经看得不少了,故而,老周不考虑一个个知识点地去写,那样会成为年度最大的屁话,何况官方文档也很详尽.老周主要扯一下大伙伴们 ...
- 【ASP.NET Core】MVC 控制器的模型绑定(宏观篇)
欢迎来到老周的水文演播中心. 咱们都知道,MVC的控制器也可以用来实现 Web API 的(它们原本就是一个玩意儿),区别嘛也就是一个有 View 而另一个没有 View.于是,在依赖注入的服务容器中 ...
- 【ASP.NET Core】MVC模型绑定——实现同一个API方法兼容JSON和Form-data输入
在上一篇文章中,老周给大伙伴们大致说了下 MVC 下的模型绑定,今天咱们进行一下细化,先聊聊模型绑定中涉及到的一些组件对象. ------------------------------------- ...
随机推荐
- 20155216 2016-2017-2 《Java程序设计》第四周学习总结
教材学习内容总结 理解封装.继承.多态的关系 封装:使用类方法或函数将程序进行封装,并定义其内部的成员以及数据. 继承:子类继承父类,避免重复的行为定义. 多态:子类只能继承一个父类,即其中存在is- ...
- 20155323刘威良第一次实验 Java开发环境的熟悉(Linux + IDEA)
20155323刘威良第一次实验 Java开发环境的熟悉(Linux + IDEA) 实验内容 1.使用JDK编译.运行简单的Java程序: 2.使用Eclipse 编辑.编译.运行.调试Java程序 ...
- WPF 带水印的密码输入框
原文:WPF 带水印的密码输入框 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/BYH371256/article/details/83652540 ...
- dsu on tree总结
dsu on tree 树上启发式合并.我并不知道为什么要叫做这个名字... 干什么的 可以在\(O(n\log n)\)的时间内完成对子树信息的询问,可横向对比把树按\(dfs\)序转成序列问题的\ ...
- Gulp 有用的地址
gulp似乎成为web开发的必选工具. 推荐一个非常好的入门教程 https://markgoodyear.com/2014/01/getting-started-with-gulp/ 官方插件列表: ...
- XAF-属性编辑器中的EditMask,DisplayFormat格式化字符串该如何设置
XAF项目中有个DisplayFormat和EditMask设置,其中: 任何地方看到的DisplayFormat都是用于显示时,即非修改状态的编辑器,显示值的格式. EditMask是指编辑时的格式 ...
- new表达式,operator new和placement new介绍
new/delete是c++中动态构造对象的表达式 ,一般情况下的new/delete都是指的new/delete表达式,这是一个操作符,和sizeof一样,不能改变其意义. new/delete表达 ...
- TW实习日记:第六天
今日的一整天都是在开发微信相关的接口,因为项目的系统是嵌在企业微信中,所以不可避免的要产生微信UserID和企业系统ID的匹配关系,那么就需要用手机号或是邮箱这种两边都存在的唯一参数进行匹配.然后再将 ...
- 袋鼠云研发手记 | 开源·数栈-扩展FlinkSQL实现流与维表的join
作为一家创新驱动的科技公司,袋鼠云每年研发投入达数千万,公司80%员工都是技术人员,袋鼠云产品家族包括企业级一站式数据中台PaaS数栈.交互式数据可视化大屏开发平台Easy[V]等产品也在迅速迭代.在 ...
- [转]Zookeeper系列(一)
一.ZooKeeper的背景 1.1 认识ZooKeeper ZooKeeper---译名为“动物园管理员”.动物园里当然有好多的动物,游客可以根据动物园提供的向导图到不同的场馆观赏各种类型的动物,而 ...