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.