函数计算新功能-----支持C#函数

前言

函数计算作为新兴的事件驱动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。如果有任何疑问和建议,欢迎留言。

上一篇:Java 线程技术之同步计数器Semaphore


下一篇:Oracle索引梳理系列(五)- Oracle索引种类之表簇索引(cluster index)