This Function Developer Kit makes it easy to deploy .NET Core functions to Fn.
Before starting, ensure you have installed Fn, and started a local Fn server by running fn start.
# Create new function mkdir MyFunction cd MyFunction fn init --init-image daniel15/fn-dotnet-init # Deploy it to local Fn server (make sure you've ran "fn start" first) fn deploy --app Test --create-app --local # Test it out! echo "Daniel" | fn invoke Test MyFunction # Should output "Hello Daniel"There are three possible ways to configure Fn functions. These are similar to how ASP.NET Core Middleware can be written:
One approach is to call FdkHandler.Handle with a lambda expression. The function takes two arguments: A context containing information about the request (for example, HTTP headers, app name, function name, etc), and the input to the function. The function can be either a regular function, or an asynchronous function using the async keyword.
FdkHandler.Handle((ctx, input) => { var name = input.AsString(); return $"Hello {name}"; });Alternatively, the function can be configured as a class. This is what fn init will give you out of the box, and is generally the recommended approach. Instead of taking the IInput, you can just have a string argument directly. Your class must have either an InvokeAsync or Invoke method.
class MyFunction { public async Task<string> InvokeAsync(string input, CancellationToken timedOut) { // ...you could do some async stuff here... return Task.FromResult($"Hello {input}"); } }The CancellationToken is important to ensure the function is properly aborted if it exceeds the configured function call timeout. You should pass it to all async methods called by your function.
For functions that do not do any async operations, you can implement Invoke instead:
class MyFunction { public string Invoke(string input) { return $"Hello {input}"; } }You can also implement the IFunction interface directly. However, this is not recommended as it has extra boilerplate:
public class FooFunction : IFunction { public Task<object> InvokeAsync(IContext ctx, IInput input, IServiceProvider services) { var name = input.AsString(); return $"Hello {name}"; } }To get the input as a strongly-typed JSON object, just add the object as a parameter to your function:
public class MyInput { public string Name { get; set; } } class MyFunction { public string Invoke(MyInput input) { return $"Hello {input.Name}!"; } }Alternatively, you can use dynamic to get the data as a dynamic object:
class MyFunction { public string Invoke(dynamic input) { return $"Hello {input.Name}!"; } }If using the lambda expression syntax, use the AsJson method on the input argument:
FdkHandler.Handle((ctx, input) => { var inputObj = input.AsJson<MyInput>(); return $"Hello {input.Name}!"; });Request headers can be obtained from the Headers property on the IContext:
class MyFunction { public string Invoke(IContext ctx) { var foo = ctx.Headers["X-Foo"]; // ... } }IContext also contains other information about the request, such as the call ID (which is unique per call), the HTTP method, the system configuration, etc.
To return "raw" data from your function, either return a string (as shown above), or a stream:
class MyFunction { public Stream Invoke() { return File.Open("/tmp/test.txt", FileMode.Open, FileAccess.Read); } }A stream is useful when returning a large response, as the entire response does not need to be cached in RAM beforehand. The stream will be automatically closed and disposed once the request completes.
Any POCOs (Plain Old C# Objects) returned from the function will be automatically serialized as JSON. This can be an anonymous type:
class MyFunction { public object Invoke() { return new { Message = "Hello World!" }; } }Example of invoking this function:
$ fn invoke Test myfunction {"Message":"Hello World!"}To set custom status codes or headers, you need to return a subclass of FnResult (RawResult, JsonResult or StreamResult, depending on the type you're returning, as documented above). This contains HttpStatus and Headers properties:
class MyFunction { public object Invoke(string input) { return new RawResult($"Hello {input}!") { // Example of custom status code HttpStatus = StatusCodes.Status202Accepted, // Example of custom headers Headers = { ["X-Some-Header"] = "foo" } }; } }To properly handle function call timeouts, any functions that perform I/O (database operations, downloading/uploading, etc) should be async functions, and use the CancellationToken provided as an argument (or in ctx.TimedOut for the lambda expression syntax) for all their async function calls. For example:
var client = new HttpClient(); var response = await client.GetAsync("https://www.example.com/", ctx.TimedOut);This will ensure that the operation is correctly cancelled if the function times out.