前言
函數計算作為新興的事件驅動serverless平台正受到越來越多開發者的歡迎,之前已支援Java, Python, Nodejs, Php四種語言。現在函數計算正式支援C#。由于其和Java類似的功能以及和Windows的緊密內建,.Net在中小企業中非常普及,許多中小企業的内部應用都是基于.Net開發。是時候讓這些傳統應用擁抱新興的serverless平台了。.Net平台中目前得到最多關注的便是.Net Core,它是目前微軟主推的真正的跨平台語言。自.Net core 2.1之後,現有的.Net程式可以非常友善的移植。目前函數計算支援的.Net版本便是.Net Core 2.1。
功能介紹
在函數計算中運作.net程式分為兩種模式,一種是以Library形式運作使用者提供的函數--我們稱之為Normal Invoke,主要面向計算場景,可以對接除http trigger以外的其他trigger;另一種是以Web App形式運作使用者提供的Asp.Net Core Web App(包括Web Api或者MVC Web App/Razer Web App),主要面向Web服務場景,需要對接Http trigger,我們稱之為Http Invoke。下面分别介紹如何開發這兩種場景的函數計算代碼。
Normal Invoke開發
在函數計算中開發Normal Invoke非常簡單,它包括一個Handler函數和一個可選的Init函數,我們支援static函數或者instance函數,以下隻顯示instance函數的簽名。
Handler API 簽名
OutputType func(InputType input) // 此處InputType和OutputType可以為Stream或者任何可序列化的對象。
OutputType func(InputType, IFcContext context) // 增加了IFcContext參數,可以獲得函數計算相關資訊(比如用來通路阿裡雲的臨時AK等)
async Task<OutputType> func(InputType input) // 當然,目前流行的Async style函數我們也一樣支援,此處傳回值也可以是async Task,下面也一樣,省去不表。
async Task<OutputType> func(InputType input, IFcContext context)
從上面的簽名可以看出,我們對于使用者代碼的限制是非常少的。如果不需要用到context的話完全不需要依賴任何函數計算的SDK,幾乎就是free style程式設計。當然我們還是推薦使用者采用帶IFcContext的函數簽名,除了能友善安全的通路阿裡雲資源之外,它自帶的RequestId以及Logger對象等都是開發者診斷調試的好夥伴,下面是IFcContext定義,起内容和其他語言類似。
public interface IFcContext
{
string RequestId { get; }
IFunctionParameter FunctionParam {get;}
IFcLogger Logger { get; }
ICredentials Credentials {get;}
string AccountId { get; }
string Region { get; }
IServiceMeta ServiceMeta { get; }
}
在工程中添加Aliyun.Serverless.Core包即可使用該接口。
初始化 API
初始化API用來完成一些全局的初始化工作---也就是說該API隻在容器服務的第一個請求時才被調用。
void Init(IFcContext context) //初始化函數不能有傳回值,因為無法傳回給使用者。如果有傳回值,那它也會被忽略
注意事項
- 我們期望使用者編寫的函數是stateless的,基于這個前提和效率方面的考慮,Handler以及Init函數所在的對象在不同的請求之間是有可能被重用的。是以說如果Handler或者Init有改變對象成員變量的行為,則該函數運作時的結果可能受到上一次調用的影響。
- 關于InputType以及OutputType,我們支援任何能被NewtonSoft.Json序列化的對象。如果該對象無法被NewtonSoft.Json序列化,則使用者需要在類的定義上指定FcASerializes屬性,以指定自定義的序列化方法,該自定義序列化實作需要随使用者代碼一起上傳。
- 初始化API和Handler API必須是public.
Http Invoke 開發
是的,使用者可以在函數計算運作Asp.Net Core程式。将原有的Asp.Net Core程式改造成能被函數計算執行的代碼也是非常簡單的,在Nuget添加Aliyun.Serverless.Core.Http之後,隻需要少量代碼便可以移植到函數計算。
最簡單的例子
假如下面是原來的Main函數---通過向導産生的代碼
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
隻需要在所在工程加入如下代碼即可:
using System;
using Aliyun.Serverless.Core.Http;
using Microsoft.AspNetCore.Hosting;
namespace YourNameSpace
{
public class FcRemoteEntrypoint : FcHttpEntrypoint
{
protected override void Init(IWebHostBuilder builder)
{
builder.UseStartup<Startup>();
}
}
}
移植一個簡單的WebApp就這麼簡單。
稍微複雜點的例子,假如我們需要在Main函數裡做一些初始化工作,如下代碼所示:
public class Program
{
public static void Main(string[] args)
{
Init1();
IWebHost host = CreateWebHostBuilder(args).Build();
Init2(host);
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
此時FcRemoteEntryPoint則應該和Main一樣,有Init1()以及Init2()的調用,如下:
using System;
using Aliyun.Serverless.Core.Http;
using Microsoft.AspNetCore.Hosting;
namespace YourNameSpace
{
public class FcRemoteEntrypoint : FcHttpEntrypoint
{
protected override void Init(IWebHostBuilder builder)
{
Init1()
builder.UseStartup<Startup>();
}
protected override void PostInit(IWebHost host)
{
Init2(host);
}
}
}
FcHttpEntrypoint剖析
這裡FcHttpEntrypoint是FC提供在Aliyun.Serverless.Core.Http包裡的基類,它起到兩個作用:1) 調用使用者的Asp.Net core初始化代碼,作用類似于本地運作的Main函數。2)包含運作使用者代碼的入口函數,用來從函數計算執行引擎獲得請求并傳遞給使用者代碼,代碼執行完成後将使用者響應傳回給函數計算執行引擎。最簡單的情形下使用者隻需要實作Init函數即可。下面詳細介紹如何重載FcHttpEntrypoint中提供的方法來實作更為複雜的功能。
PostInit
如果需要對這個IWebHost對象在Init()之後、Start()之前做一些初始化操作(比如用DependencyInjection獲得一些内部的Service做初始化),那應該重載PostInit函數。
protected virtual void PostInit(IWebHost host) { }
PostMarshallRequestFeature
如果需要對請求做一些額外的處理,可以在PostMarshallRequestFeature中進行
protected virtual void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, HttpRequest request, IFcContext fcContext)
PostMarshallResponseFeature
如果需要對響應做一些額外的處理,可以在PostMarshallResponseFeature中進行
protected virtual void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, HttpResponse response, IFcContext fcContext)
HandleRequest
HandleRequest是函數計算請求的入口函數,它将請求Mashall成Asp.net core的請求之後傳給使用者代碼進行處理,再将使用者代碼傳回的響應Marshall成函數計算響應傳回給使用者。在大部分情況下使用者不需要重載這一函數。目前唯一需要重載的情況是如果使用者設定自定義域名時還包括一個虛拟目錄字首(比如
http://abc.com/a/),由于函數計算的代碼無法區分一個URL中哪個部分是虛拟目錄,需要使用者将request中的Path分成PathBase以及Path兩部分,下面是示例代碼
public override async Task<HttpResponse> HandleRequest(HttpRequest request, HttpResponse response, IFcContext fcContext)
{
AdjustRequestPath(request); // set request.Path and request.PathBase correctly. User needs to implement it according to the virtual directory.
return base.HandleRequest(request, response, fcContext);
}
- 在函數計算中,任何檔案的寫操作均應該指向NAS盤,本地檔案由于各個container之間無法共享,是以無法儲存程式的狀态。
- 如果Web工程依賴作業系統相關的native元件,用dotnet publish指令生成的publish檔案夾下會産生runtimes目錄。而FC預設的bin目錄是代碼所在目錄或者代碼所在目錄的lib目錄,是以需要手工将目錄linux-x64/native下的.so檔案複制到publish目錄(或者publish/lib目錄),然後再打包。
- 目前函數計算要求Web App有自己的自定義域名,否則傳回的内容将以附件形式傳回,不友善調試。一個繞過的辦法是用Chrome一個叫Undisposition的插件自動删除Content-Disposition:Attachment。
開發體驗
下面以Normal Invoke為例開發一個簡單OSS Event Handler。
建立空白工程
mkdir FcExample
cd FcExample
dotnet new console
或者
dotnet new classlib
添加FC類庫引用
編輯FcExample.csproj,添加如下代碼
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<AssemblyName>FcExample</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aliyun.Serverless.Core" Version="1.0.1" />
<PackageReference Include="Aliyun.OSS.SDK.NetCore" Version="2.9.1" />
<PackageReference Include="Aliyun.Serverless.Core.Mock" Version="1.0.1" />
</ItemGroup>
</Project>
編寫函數
編輯Program.cs,添加如下代碼
using System;
using System.IO;
using Aliyun.OSS;
using Aliyun.Serverless.Core;
using Aliyun.Serverless.Core.Mock;
using Microsoft.Extensions.Logging;
namespace FcExample
{
public class OssFileHandlerRequest
{
public string Bucket;
public string Key;
public string Endpoint;
}
public class OSSFileHandler
{
public Stream GetOssFile(OssFileHandlerRequest req, IFcContext context)
{
if (req == null)
{
throw new ArgumentNullException("req");
}
if (context == null || context.Credentials == null)
{
throw new ArgumentNullException("context");
}
context.Logger.LogInformation("GetOssFile started. {0}", context.Credentials.AccessKeyId);
OssClient ossClient = new OssClient(req.Endpoint, context.Credentials.AccessKeyId, context.Credentials.AccessKeySecret, context.Credentials.SecurityToken);
OssObject obj = ossClient.GetObject(req.Bucket, req.Key);
return obj.Content;
}
}
class Program
{
static void Main(string[] args)
{
string bucket = "<bucket name>";
string key = "<key>";
string id = "<id>";
string secret = "<key>";
string endpoint = "<oss endpoint>"; // http://oss-cn-hangzhou.aliyuncs.com
string accountId = "FakeAccountId";
string reqId = "RequestId123#" + DateTime.UtcNow;
OSSFileHandler handler = new OSSFileHandler();
FcContext context = new FcContext(accountId, reqId);
Credentials credentials = new Credentials();
credentials.AccessKeyId = id;
credentials.AccessKeySecret = secret;
context.Credentials = credentials;
Stream stream = handler.GetOssFile(new OssFileHandlerRequest() { Bucket = bucket, Key=key, Endpoint= endpoint}, context);
// verify stream
stream.Close();
}
}
}
- 入口函數不能被overload--也就是說不允許在類中包含其他同名的函數。
- 一般推薦本地測試函數在另一個工程中,在産品代碼中不需要引用Aliyun.Serverless.Core.Mock,以減小代碼包體積。
- Http Invoke的函數是不能當做Normal Invoke來調用的,反之也亦然。
測試函數
在Main()中輸入測試賬号以及bucket、key資訊,加入适當的檢查stream的邏輯後,運作
dotnet run
即可測試
打包
運作
dotnet publish -c Release
,然後
cd bin/Release/netcoreapp2.1/publish
,最後
zip code.zip *
打包時確定所有依賴的dll檔案在zip檔案的根目錄或者名為lib的子目錄中,否則該依賴檔案可能無法被加載
釋出函數
去函數計算控制台建立一個dotnetcore2.1函數,上傳code.zip,并指定函數Handler為
FcExample::FcExample.OSSFileHandler::GetOssFile
小結
本文簡單介紹了如何編寫能運作在函數計算中的C#函數,包括normal invoke和Http invoke。如果有任何疑問和建議,歡迎留言。