A+ A A-

Getting started with Azure Function Apps in Delphi

In extension to a recent Webinar abut the same topic I did together with Stephen Ball from Embarcadero (Watch here), I’ve condensed and added additional information I was unable to show during the initial Webinar in the following blog post. This is intended as a primer to get started with Azure Function Apps and Delphi.
Azure Function Apps is a handy component in the category of “serverless”, part of the Microsoft Azure Cloud Platform.

Authors opinion:
Personally I don’t subscribe to the buzzword “Serverless” as we do need servers to make things work, rather it’s a managed event driven framework which abstracts some of the manual labor away with dynamic resource allocation and so on.
“Serverless” is a sellable buzzword for less management cost – but it does require servers at the end of the day.
Call it what it is - managed event driven modules.
Plan accordingly, and evaluate carefully, as “Serverless” isn’t the holy grail for every scenario.

In a nutshell, Azure Function Apps are self-contained web servers that respond to an event – an api endpoint and allows you to scale up and down based on demand without you having to worry about a lack of resources.

Not too long ago, Microsoft added support for running a native compiled binary, granted it follow certain conventions, as a Function App. They demonstrated this by showing how you could implement a custom http trigger in Go and Rust, both compiled languages, however these languages don’t offer the same advantages as Object Pascal does. This means you’re not limited to a managed language like C# or interpreted languages like JavaScript or Python.

In production you can either target a 64-bit Linux or Windows Function App Runtime – thankfully Delphi conveniently have compilers for both targets!

Before we get started, we need to install a few prerequisites. To avoid “Don’t Repeat Yourself”, I will be linking to setup guides from the Microsoft documentation, as they already have covered the basics well. The first section is setting up your environment, but in essence we need:

  • Azure Functions Core Tools – A local version of the Function Apps runtime also found in the cloud.
    - In this demo I’m using the 32-bit version of the Function Core Tools framework, but for production it is 64-bit only.
  • Visual Studio Code – For the time being, until my RAD Studio Azure plugin is ready, VS Code has the Azure Extension which makes it easier to create and deploy Function Apps.
  • Azure Functions Extension for VS Code – Connects to your Azure Cloud account and makes it easier to create and deploy Azure Functions

As we’re using Delphi, we won’t need either the Go or Rust elements.

Once the prerequisites are installed, either clone the demo projects from this GitHub repository, or set up a new blank Azure Function Apps custom handler project, following the guide outlined here

For project management simplicity I would encourage you to follow this folder structure when creating an Azure Function App in Delphi:

FuncApp1

DelphiFunctionApp – This folder contains the actual Delphi project. The project file has been set up to output the binary one level above. Go to Project Options -> Building -> Delphi Compiler and set the Output directory to ..\

DelphiTrigger – This folder contains the Azure Function descriptor “function.json” which instructs the Function Runtime how to handle this custom trigger and what methods the function app supports.

“host.json” – This file instructs the Function Runtime how to load our custom HTTP trigger (Our Delphi app) – the “defaultExecutablePath” and in this case, also instructs the Function Runtime to forward the HTTP requests with the setting “enableForwardingHttpRequest”: true – for our purpose it greatly simplifies development.
Remember to rename the name of the executable according to what Function App Runtime stack (Windows or Linux) you’re executing on in the production environment.


FuncApp2

For now, we won’t focus on the rest of the files listed in the directory.

 

Now let’s write some Delphi code!

In these demo projects, I’ll use the Standalone WebBroker project template built directly into Delphi, based around the Indy components. For a future post, we will be investigating other compact webserver offerings for Delphi.

The first demo project, “Example001_SimpleHelloWorld” we will start with an empty WebBroker project, trim out unnecessary code, add a Linux target for convenience, and test that the local Function Runtime is able to execute the code and emit a HelloWorld JSON string to the browser.

In the second demo project, “Example002_ParsingRequestParameters” we’ll explore how to parse provided URL parameters and return a response based on the parsed input.

If you don’t set the enableForwardingHttpRequest option to true, the Function Runtime will send a JSON payload to your http handler (your Delphi app). In a future blog post we’ll explore how you can parse this payload and manage the data received.

Common for a standalone console webbroker template, is the interactive menu.
We want the web server component to boot up immediately, so we are going to cut out the additional fluff added, which leaves us with the methods:

BindPort()

CheckPort()

StartServer()

StopServer()

RunServer()

And the main application entry point.

Handling the request happens in the default webmodule created in the DefaultHandlerAction.

procedure TwmAzureFunction.WebModule1DefaultHandlerAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
const
  cJSON = 'application/json';
var
  LJSONObject: TJSONObject;
begin
  LJSONObject := TJSONObject.Create;
  try
    LJSONObject.AddPair('HelloWorldFunction', TJSONBool.Create(False));
    if Request.PathInfo.Equals('/api/DelphiTrigger') then
      begin
        Response.ContentType := cJSON;
        Response.Content := LJSONObject.ToJSON;
        FunctionRequested := True;
      end;
  finally
    LJSONObject.Free;
  end;
end;

 

Once you have implemented your DefaultHandlerAction and everything is set up, compile the project to produce an exe file.

Open a command prompt or PowerShell window, and navigate to the root folder of the Function Apps project (Where you have the host.json file). Run the command: func start --verbose
If everything is set up, the Function Apps Runtime will boot up and load our custom HTTP trigger and webbroker executable.
Try opening a browser and navigate to the URL listed in the output of your command prompt / PoweShell window.


FuncApp3

You should see the Function App Runtime receiving the request, forwarding it to our Delphi app and return the response our Delphi app forwarded.

In our second example, where we handle the parameters, provided in the URL, we reuse most of our existing hello world example.

The change is how the parameter URL is provided, and how we handle the Request in our WebModule.

By modifying our DefaultHandlerAction we can check whether additional queries have been added.
If we provide these parameters here “?Name=Glenn&Age=32&Adult=True” appended to our trigger, we get a valid JSON response.
If we’re not providing the correct parameters, we’ll get an error.

procedure TwmAzureFunction.WebModule1DefaultHandlerAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
const
  cJSON = 'application/json';
var
  LJSONObject: TJSONObject;
begin
  LJSONObject := TJSONObject.Create;
  try
    if Request.PathInfo.Equals('/api/DelphiTrigger') then
      begin
        if Request.QueryFields.Count = 3 then
          begin
            LJSONObject.AddPair('PersonName', TJSONString.Create(Request.QueryFields.Values['Name']));
            LJSONObject.AddPair('PersonAge', TJSONNumber.Create(Request.QueryFields.Values['Age'].ToDouble));
            LJSONObject.AddPair('PersonIsAdult', TJSONBool.Create(Request.QueryFields.Values['Adult'].ToBoolean));
          end
        else
          LJSONObject.AddPair('Error', 'Not enough parameters');
        Response.ContentType := cJSON;
        Response.Content := LJSONObject.ToJSON;
        FunctionRequested := True;
      end;
  finally
    LJSONObject.Free;
  end;
end;

 

 

For greater control, you can even send JSON objects!

Reach out to me on Twitter if you have further questions!



Delphi Managed Records - Making your code smart

With Delphi 10.4, Managed Records language feature was introduced. As some might remember, it was originally intended for Delphi 10.3, but Embarcadero wisely decided to postpone the feature, to ensure a solid implementation - and now we have it!

It is important to mention, this is a language feature available with Delphi 10.4 and newer.

Managed Records opens up many new possibilities to write smarter and more effective code without syntactic sugar and bloat which other popular languages suffer from. This is one of the many reasons I keep using Delphi - the language is simple, yet powerful and allows you to eloquently write software.
Writing software is after all an art, just like writing a novel.

Anyways, back to the actual topic - Managed Records.

In this article, I want to share a little clever tidbit of how you can utilize the automatic initalization and finalisation calls of Managed Records. 
INI files is still one of the easiest ways to have a key-value settings storage across platforms, and usually it takes some legwork to create the object, read or write your settings and ensuring the object is destroyed again.
What if you could access the INI Read or Write features right away and don't worry about keeping track of the object life-cycle?
- Managed Records to the rescue!

In a recent feedback tool I've been working on for another client project, I had the need to save and load a few settings throughout the application.
I figured I didn't want to do the classic create object, do my operation in a try-finally block and close down the object again and thought that Managed Records automatic initialization and finalization should be able to do the trick.
The following snippet illustrates how this can be done:

unit MRTest.AppSettings;

interface

uses
  System.SysUtils,
  System.Classes,
  System.IOUtils,
  System.IniFiles;

type
  TAppSettings = record
  strict private const
    cIniFileName: string = 'TestSettings.ini';
  strict private
    FAppConf: TMemIniFile;
    class function TAppSettings.InternalGetFullIniFile: string; static;
  public
    class operator Initialize(out ADest: TAppSettings);
    class operator Finalize(var ADest: TAppSettings);
    property AppConf: TMemIniFile read FAppConf write FAppConf;
  end;

implementation

{ TAppSettings }

class operator TAppSettings.Initialize(out ADest: TAppSettings);
begin
  ADest.FAppConf := TMemIniFile.Create(ADest.InternalGetFullIniFile);
  ADest.FAppConf.AutoSave := True;
end;

class operator TAppSettings.Finalize(var ADest: TAppSettings);
begin
  ADest.FAppConf.Free;
end;

class function TAppSettings.InternalGetFullIniFile: string;
begin
  Result := TPath.Combine(TPath.GetDocumentsPath, cIniFileName);
end;

end.

 

Now you can just include the unit in your project where needed and automatically allocate a variable of the type TAppSettings (Or whatever name your record type has).
The example below has been reduced for the simplicity to give you a gist of how it can be approached.

uses
MRTest.AppSettings;


procedure LoadSettings;
begin
  var LApp: TAppSettings;
  CheckBox1.Checked := LApp.AppConf.ReadBool('Settings', 'FirstStart', True);
end;

procedure SaveSettings;
begin
  var LApp: TAppSettings;
  LApp.AppConf.WriteBool('Settings','FirstStart', CheckBox1.Checked);
end;

As you see, everything is encapsulated in the record type and the managed nature automatically takes care of creating the INI object with the correct settings when it goes into scope, and unloads it again once it goes out of scope.

I hope you found this information useful.




We use cookies on our website. Some of them are essential for the operation of the site, while others help us to improve this site and the user experience (tracking cookies). You can decide for yourself whether you want to allow cookies or not. Please note that if you reject them, you may not be able to use all the functionalities of the site.