πΊοΈ Overview
We can use Azure WebJob SDK to develop a Azure Functions Custom Extension for FTP Triggers and Bindings.
1οΈβ£ Trigger
Triggers cause a function to run. A trigger defines how a function is invoked and a function must have exactly one trigger. Triggers have associated data, which is often provided as the payload of the function.
2οΈβ£ Binding
Binding to a function is a way of declaratively connecting another resource to the function; bindings may be connected as input bindings, output bindings, or both. Data from bindings is provided to the function as parameters.
- Input binding receives the data from the external source.
- Output binding sends the data to the external source.
More information about these two concepts can be found here.
π Requirements
The requirements for building this custom Azure Function FTP Extensions are:
We need a Ftp Triggers on:
- Single FtpFile or FtpStream
- Multiple FtpFiles or FtpStreams (= batch support)
And we need Ftp Bindings for:
- FtpFile
- FtpStream
- IFtpClient
- AsyncCollector
π’ Building Blocks
Intro
In the next chapters I’ll highlight and explain all the components which are needed to build an FTP Azure Function Extension.
1. FTP Client
For connecting and listening to an FTP server a C# .NET FTPClient is needed, for this purpose I used FluentFTP:
FluentFTP is a fully managed FTP and FTPS library for .NET & .NET Standard, optimized for speed. It provides extensive FTP commands, File uploads/downloads, SSL/TLS connections, Automatic directory listing parsing, File hashing/checksums, File permissions/CHMOD, FTP proxies, FXP transfers, UTF-8 support, async/await and more. |
FTP Trigger
To create a custom Trigger, we need to build these components:
2. TriggerAttribute
In an Azure Function, the triggers and bindings are defined by an Attribute class. This class holds all parameters and configuration values which are needed by the extension.
Define a class that extends from Attribute
. This class represents our attribute class. We define all the parameters and configuration values for our trigger.
In our case, we define the ConnectionString and properties.
- FtpTriggerAttribute : This is the Attribute which the consumer should use to make the Azure Function trigger on an FTP event.
- Connection (name or complete url) : The mandatory connection can be defined as a name (which should be defined in the ConnectionStrings section from a config file, or it can be the full connection string to connect to to the FTP server)
- Configuration properties : Some properties are mandatory like the folder and polling interval. In addition there are also some optional properties.
- The parameter : This is the parameter which receives the data when the trigger is triggered and sends data to this Azure Function.
Note that only a predefined list of types are supported here, e.g.
FtpFile
,FtpFile[]
,FtpStream
andFtpStream[]
.
First create an AbstractBaseFtpAttribute
. This is an abstract base class because it’s used for trigger and binding attributes.
This class AbstractBaseFtpAttribute looks as follows:
|
|
The FtpTriggerAttribute
which extends the AbstractBaseFtpAttribute
and looks as follows:
|
|
3. IListener
Define a class that implements the interface IListener
.
The IListener
interface has the following methods:
- StartAsync: - the Function Runtime calls this method to start our listener. This method returns one Task object that completes when our listener successfully started.
- StopAsync: - the Function Runtime calls this method to stop our listener. This method returns one Task object that completes when the listener completely stopped.
- Cancel: - the Function Runtime calls this method to cancel any ongoing listen operation.
- Dispose: - IDisposable’s dispose method.
The implementation class (FtpListener) from this interface contains the following logic:
- Connect to the FTP server
- FTP Listing
- Recursive (gets files within subdirectories as well)
- Detect changes since last run
- Get
- FtpFile(s)
- FtpStream(s)
- Process
- Single FtpFile or FtpStream
- Batch for FtpFiles or FtpStreams
The FtpListener class receives four parameters:
ILogger
; The loggerType
; The Trigger value type (which can only be a FtpFile or FtpStream)ITriggeredFunctionExecutor
; executorFtpTriggerContext
; context
The FtpListener
class uses the FtpClient
(from FluentFTP) to listen to the FTP server. When data is received (in this implementation this is a FtpFile or FtpStream), we invoke the function using the ITriggeredFunctionExecutor
instance.
For the full code from the FtpListener
class, see this link.
4. ITriggeredFunctionExecutor
We use the ITriggeredFunctionExecutor
instance to execute the triggered function when data (FtpFile or FtpStream) is received.
The FtpTriggerContext
object has two member variables:
- The
TriggerAttribute
variable is an object of ourAttribute
class which used in the Azure Function and contains all the configuration (ConnectionString and other properties). - The
Client
variable is an object of theIFtpClient
interface which represents the FtpClient instance to connect FTP.
The context class looks as follows:
|
|
5. ITriggerBinding
Define a class that implements the interface ITriggerBinding
. In this class, we create our listener and bind our trigger data.
The ITriggerBinding
-interface has the following methods:
CreateListenerAsync: - the Function Runtime calls this method to create a listener. This method returns a Task object that has our listener.
BindAsync: - This method is called to bind a specified value using a binding context. When our listener receives an event, we try to execute the function, passing the event data. This event data is encapsulated in a
TriggeredFunctionData
class and send to the Azure Function. In theBindAsync
, we will bind this value to our corresponding data. This method returns aTriggerData
class. TheTriggerData
class accepts a class that implements anIValueBinder
interface and a read-only dictionary, this will revisited later in this article.ToParameterDescriptor: - the Function Runtime calls this method to get a description of the binding. This description is visible when running the Azure Function locally:
And also when deployed in Azure:
6. ITriggerBindingProvider
Define a class that implements the interface ITriggerBindingProvider
.
This class is a provider class that returns a class that implements the ITriggerBinding
interface. This class has the following methods:
- TryCreateAsync: - When functions are being discovered this method wil be called to get a class that implements the
ITriggerBinding
interface. the Function Runtime will pass aTriggerBindingProviderContext
class as a parameter. In this method, we check whether theTriggerBindingProviderContext
object contains our custom attribute and if the parameter has a valid type (e.g. FtpFile or FtpStream). If theAttribute
is present and the parameter type is valid, a TriggerBinding class is created.
7. IValueBinder
Define a class that implements the interface IValueBinder
.
As explained in the BindAsync
section, we are binding the trigger data to our data class using this class. The IValueBinder
has three methods:
- GetValueAsync: - Returns a task that has the value object.
- SetValueAsync: - Returns a task that completes when the object to our data class completes.
- ToInvokeString: - Returns object as string. In case of a single FtpFile or FtpStream this is the FullName, in case of multiple objects (batch of FtpFiles or FtpStreams), this is a comma separated string from all the FullNames.
8. IExtensionConfigProvider
Create a class that implements the interface IExtensionConfigProvider
. The IExtensionConfigProvider
defines an interface enabling third party extension to register.
This interface has the following method:
Initialize: - in this method, all the triggers and bindings are registered, see code excerpt below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public void Initialize(ExtensionConfigContext context) { // 1. Add trigger var triggerRule = context.AddBindingRule<FtpTriggerAttribute>(); triggerRule.BindToTrigger(new FtpTriggerBindingProvider(_logger, this)); // 2. Add bindings var bindingRule = context.AddBindingRule<FtpAttribute>(); // 2a. Add Input-Binding bindingRule.BindToInput<IFtpClient>(typeof(FtpBindingConverterForIFtpClient), this); // 2b. Add IAsyncCollector Output-Binding for FtpFile and FtpStream var arguments = new object[] { _logger, this }; bindingRule.BindToCollector<FtpFile>(typeof(FtpBindingConverterForIAsyncCollector<>), arguments); bindingRule.BindToCollector<FtpStream>(typeof(FtpBindingConverterForIAsyncCollector<>), arguments); }
9. IWebJobStartup
And finally, we create a class that implements the interface IWebJobStartup
. This interface defines the configuration actions to perform when the Function Host starts up.
This interface has the following method:
Configure: - the Function Runtime call this method when the function host initializes. in this method, we will add our custom extension.
This startup class looks like this:
1 2 3 4 5 6 7 8 9 10
public class FtpBinding { public class Startup : IWebJobsStartup { public void Configure(IWebJobsBuilder builder) { builder.AddFtp(); } } }
Note the AddFtp
method. This method is an extension function of IWebJobsBuilder
and is defined in the FtpWebJobsBuilderExtensions
class.
The AddFtpExtension
is a helper method.
The FtpWebJobsBuilderExtensions
class looks like this.
|
|
As you can see in this extension method, we are adding the extension using the AddExtension
method of the IWebJobsBuilder
.
The AddExtension
method takes one parameter, our FtpExtensionConfigProvider
instance.
We are also adding a Singleton Service for the FtpClientFactory.
The constructor of the FtpExtensionConfigProvider
instance will receive this server as a parameter.
So basically what happens is when the Function Runtime starts, it searches for a class that implements IWebJobStartup
. When it found a class that implements the interface:
the Function Runtime calls the Configure method passing the
IWebJobsBuilder
object. We add the extension using theAddExtension
method using the class that implements theIExtensionConfigProvider
interface.the Function Runtime calls the Initialise method of the
IExtensionConfigProvider
passingExtensionConfigContext
as a parameter. Our implementation of the Initialize method adds the add the binding rule using theAddBindingRule
method of theExtensionConfigContext
, which returns aBindingRule
object. We call theBindToTrigger
method to add our trigger passingTriggerBindingProvider
as a parameter.After that system calls the
TryCreateAsync
function of theTriggerBindingProvider
passing theTriggerBindingProviderContext
as a parameter, in thisTryCreateAsync
method, we check whether ourAttribute
class present or not. A class that implements theITriggerBinding
interface is created and a Task that contains the object is returned.the Function Runtime then calls the
CreateListenerAsync
method of our class that implements theITriggerBinding
interface passingListenerFactoryContext
object. In ourCreateListenerAsync
, we return a class that implements theIListener
interface. TheListenerFactoryContext
object contains a class that implements theITriggeredFunctionExecutor
interface. TheITriggeredFunctionExecutor
interface has a method calledTryExecuteAsync
. Using this method, we can execute the triggered function, passing the event data andCancellationToken
.
FTP Binding
To create a custom Binding, we need to build these additional components:
10. BindingAttribute
Also for the binding, a class is required to represent our attribute class. We define all the parameters and configuration values for our binding. In our case, we define the ConnectionString and some extra properties.
- FtpBindingAttribute : this is the Attribute which should be used by a consumer to indicate that this is an FTP Binding.
- Connection (name or complete url) : This property is mandatory and can contain either the name of a corresponding ConnectionString in the config file, or it can be the full connection string to the FTP server.
- Configuration properties : Some properties are mandatory like the folder. In addition there are also some optional properties.
- The parameter : This is the parameter which receives the data when the binding is used in the Azure Function.
Note that only a predefined list of types are supported here, e.g.
FtpFile
,FtpStream
,AsyncCollector
andIFtpClient
.
Here is our FtpAttribute
class (which extends the AbstractBaseFtpAttribute
):
|
|
Just like in the trigger, we have connection string (from the base-class) and a folder member variable.
8. IAsyncCollector
Create a class that extends the IAsyncCollector
interface. This interface defines methods to AddAsync
. The business logic can call the AddAsync
function to send data to external resources (the FTP Server).
This class should handle FtpFile
and for FtpStream
and looks like:
|
|
β - For the full code from this class, see Bindings/FtpAsyncCollector.cs.
ποΈ Class diagram overview
When all code is in place, you end up with the following class-diagram:
π§ͺ Testing
Testing the FTP Trigger
Now we need to create a sample function to test our trigger. Let’s create a test Azure Function that uses our trigger. Our sample function looks like this:
|
|
This method is straightforward, just log an informational message when this trigger is triggered by a new FtpFile on the FTP server.
The ConnectionString
is defined in the local.settings.json
file, and the Folder
is hard-coded as a string (‘inbox’).
Example local.settings.json configuration file:
|
|
Before running our function, we need to run an FTP server for testing (e.g. FileZilla).
For more examples, see Trigger.Sample.Ftp.
Testing the FTP Binding
Let’s create a sample function to test our binding. Our sample function looks like this:
|
|
Note that this example function is triggered by a HttpTrigger and will get the message from the query and create a new FtpFile with name “stef-ftpfile.txt” on the FTP Server defined by the Connection “FtpConnection” (which is defined in the configuration file).
Unit-Testing the FTP Binding
The example below shows how to unit test a FTP Binding which uses an IAsyncCollector
to add FTP Files to the FTP server.
Example Azure Function Code
The example Azure Function below is triggered by a HttpTrigger and will get the message from the query and append a new FtpFile with name “stef-asynccollector.txt” on the FTP Server using the AddAsync
method from the IAsyncCollector<FtpFile>
interface.
|
|
Unit test code
The related unit-test looks as follows:
|
|
- Arrange the things needed to unit-test the Azure Function. So create a Mock for the
HttpRequest
and set the value of the query parameter “message” to “test”. - Create a Mock object from the
<IAsyncCollector<FtpFile>
, this Mock will be called with theAddAsync(FtpFile item)
method. - Execute the method (RunBindingIAsyncCollectorAsync) on the System Under Test (FtpBindingsSample). Note that the requestMock and collectorMock are passed here. The logger is not tested here, so I just supply a default mocked object for ILogger.
- Assert (check) if the return value is indeed a valid response.
- Verify if the
AddAsync
method is indeed called with the correct parameter (= the FtpFile) and check if this FtpFile has the indeed the correct Name and byte-content.
π» Software
- A NuGet package WebJobs.Extensions.Ftp can be downloaded here.
- And the GitHub project can be found here. In case you have questions or remarks, please create an issue there.
π‘ Conclusion
This blog post describes step by step how to build a custom azure function extension (e.g. WebJobs.Extensions.Ftp). However when following the steps defined above, any external system can be handled by an Azure Function Trigger and Binding.
πΊ Presentation
I did give a presentation (in Dutch) about ‘Azure Function Custom Extensions’ during the .NET Assemble! @ mStack, check out the recording below:
π Thanks
I want to thank Krishnaraj Varma for his excellent GitHub project NATS Custom Extension for Azure Functions which I used as a starting point to develop this WebJobs.Extensions.Ftp project and write this blogpost.
β Feedback or questions
Do you have any feedback or questions regarding my .NET Assemble! presentation or one of my blogs? Feel free to contact me!
π Additional resources
- https://github.com/robinrodricks/FluentFTP
- https://www.tpeczek.com/2018/11/azure-functions-20-extensibility_20.html
- https://geradegeldenhuys.net/read/custom-azure-function-trigger/