Creating a minimal Web API

This example shows how to create a simple HTTP function exposing RestAPI endpoints using FunctionGraph.

Prerequisites

  • .NET SDK 8.0 installed.

Source

Source for this sample can be found in: samples-doc/http_minimalWebAPI.

C# project structure

The project structure is as follows:

Project Structure
/http_minimalWebAPI/
 ├── src/
 |   ├── Properties
 |   │   └── launchSettings.json
 |   ├── wwwroot
 |   │   └── favicon.ico
 |   ├── appsettings.json
 |   ├── appsettings.Development.json
 |   ├── http_minimalWebAPI.csproj
 │   └── Program.cs
 (└── http_minimalWebAPI.sln)

C# Project file

To create a minimal Web API project for FunctionGraph,

  1. Run the following command to create a new ASP.NET Core Web API project:

    dotnet new web -o http_minimalWebAPI --use-program-main --no-https
    
  2. Navigate to the project directory:

    cd http_minimalWebAPI
    
  3. Replace the contents of the Program.cs file with the code of next section.

  4. Modify the project file (.csproj) to include the necessary settings for FunctionGraph deployment. You can refer to the provided http_minimalWebAPI.csproj file for guidance.

    http_minimalWebAPI.csproj
    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
      </PropertyGroup>
    
      <!-- ####### add below to your .csproj ###### -->
    
      <PropertyGroup>
        <ApplicationIcon>wwwroot/favicon.ico</ApplicationIcon>
      </PropertyGroup>
    
      <ItemGroup>
        <!-- Include README.md in package if exists in parent folder -->
        <Content Include="../README.md" Condition="Exists('../README.md')">
          <Pack>true</Pack>
          <PackagePath>\</PackagePath>
          <IncludeInPackage>true</IncludeInPackage>
          <CopyToOutput>true</CopyToOutput>
          <BuildAction>Content</BuildAction>
          <copyToOutput>true</copyToOutput>
          <CopyToOutputDirectory>Always</CopyToOutputDirectory>
          <CopyToPublishDirectory>Always</CopyToPublishDirectory>
        </Content>
      </ItemGroup>
    
      <!-- ## bootstrap file content -->
      <PropertyGroup>
        <GeneratedBootstrapFile>
    <![CDATA[
    export CREATE_DATE="$([System.DateTime]::Now.ToString("yyyy-MM-ddTHH:mm:ssZ"))"
    # functiongraph requires to listen on port 8000
    export ASPNETCORE_HTTP_PORTS=8000
    # set content root to $RUNTIME_CODE_ROOT
    export ASPNETCORE_CONTENTROOT=$RUNTIME_CODE_ROOT
    # set generated xml doc file to be readable
    chmod +r $(AssemblyName).xml
    # start the application
    $RUNTIME_CODE_ROOT/$(MSBuildProjectName)
    ]]>
        </GeneratedBootstrapFile>
      </PropertyGroup>
    
      <!-- ## create zip with bootstrap -->
      <Target Name="ZipOutputPath" Condition="'$(TargetFramework)'!=''" AfterTargets="Publish">
        <!-- create bootstrap file -->
        <WriteLinesToFile File="$(OutputPath)\publish\bootstrap" Lines="$(GeneratedBootstrapFile)"
          Overwrite="true" />
    
        <!-- zip the publish directory -->
        <ZipDirectory SourceDirectory="$(OutputPath)\publish"
          DestinationFile="$(MSBuildProjectDirectory)\$(MSBuildProjectName)_$(TargetFramework).zip"
          Overwrite="true" />
      </Target>
    
    
      <!-- ## publish settings for Release build -->
      <PropertyGroup Condition="'$(Configuration)' == 'Release'">
        <!-- dotnet publish -c Release -->
        <!-- <PublishReadyToRun>true</PublishReadyToRun> -->
        <PublishSingleFile>true</PublishSingleFile>
        <!-- DO NOT USE <PublishTrimmed> Controller will not work properly -->
        <!-- <PublishTrimmed>true</PublishTrimmed> -->
        <SelfContained>true</SelfContained>
        <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
        <EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
      </PropertyGroup>
    
    
      <ItemGroup>
        <PackageFrameworkReference Include="Microsoft.AspNetCore.App" />
      </ItemGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
        <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
        <PackageReference Include="Serilog" Version="4.3.0" />
        <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
        <PackageReference Include="Serilog.Expressions" Version="5.0.0" />
        <PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
        <PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
        <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
        <PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
      </ItemGroup>
    
      <PropertyGroup>
        <GenerateDocumentationFile>true</GenerateDocumentationFile>
        <NoWarn>$(NoWarn);1591</NoWarn>
      </PropertyGroup>
    
      <ItemGroup>
        <!-- favicon for swagger -->
        <None Include="wwwroot\*" />
      </ItemGroup>
    
    </Project>
    

    Using above settings ensures that the project is built correctly for deployment to FunctionGraph:

    • bootstrap file is created,

    • output is packaged in a ZIP file.

C# Program file

Program.cs
namespace http_minimalWebAPI;

using System;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Serilog;
using Serilog.Context;
using Serilog.Core;
using Serilog.Expressions;
using Serilog.Extensions.Hosting;
using Serilog.Formatting.Compact;
using Serilog.Formatting.Json;
using Serilog.Templates;

public class Program
{

  public static void Main(string[] args)
  {

    // Create the WebApplication builder
    var builder = WebApplication.CreateBuilder();

    // timezone conversion functions for Serilog.Expressions
    var dateTimeFunctions = new StaticMemberNameResolver(typeof(DateTimeFunctions));

    // Configure Serilog to log requestId and timestamp in UTC
    builder.Host.UseSerilog((context, loggerConfig) =>
        loggerConfig
        .WriteTo.Async(a => a.Console(
          new ExpressionTemplate(
            "{ { ts: ToUtc(@t), requestId: XCFFRequestId, lvl: @l,  msg: @m, threadId: ThreadId } }\n",
            nameResolver: dateTimeFunctions
          )
        ), bufferSize: 50)
        .Enrich.FromLogContext()
        .Enrich.WithThreadId()
    );

    // Add controller services
    builder.Services.AddControllers();

    // Add Swagger services
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen(options =>
    {
      options.SwaggerDoc("v1", new OpenApiInfo { Title = "Minimal Web API", Version = "v1" });

      var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
      var xmlPath= Path.Combine(AppContext.BaseDirectory, xmlFilename);
      // Console.WriteLine($"########################################### Including XML comments from: {xmlPath}");

      options.IncludeXmlComments(xmlPath);

    });

    var app = builder.Build();

    // Use the custom logging middleware
    app.UseMiddleware<LoggingMiddleware>();

    // Enable serving static files (including favicon.ico from wwwroot)
    app.UseStaticFiles();

    // Enable Swagger middleware
    string useSwaggerUI = (Environment.GetEnvironmentVariable("USE_SWAGGER_UI") ?? "false").ToLower();
    if (useSwaggerUI.Equals("true"))
    {
      app.UseSwagger();
      app.UseSwaggerUI();
    }

    // Map API endpoints (e.g in case of not using controllers)
    APIEndpoints.Map(app);

    // Map controller routes
    app.MapControllers();

    // Start the application
    app.Run();
  }

  public static class APIEndpoints
  {
    public static void Map(WebApplication app)
    {
      // Minimal API endpoint definitions
      app.MapGet("/", () => "Hello World!");


      // Route parameter example: /greeting/John
      app.MapGet("/greeting/{name}", (string name) =>
      {
        Log.Information("Greeting {Name}", name);
        return $"Hello, {name}!";
      });


      // Query parameter example: /hello?name=John
      app.MapGet("/hello", (string? name) =>
      {
        return $"Hello, {name ?? "Guest"}!";
      });

      app.MapGet("/test", async context =>
      {
        string? requestId = context.Request.Headers["x-cff-request-id"];

        await context.Response.WriteAsync($"Request ID is: {requestId}");
      });
    }
  }

}

To run this example locally, follow these steps:

Run the HTTP Function locally

To run the project, execute the following step:

dotnet run

Open your browser and navigate to http://localhost:8000 to see the “Hello, World!” response.

Other endpoints are:

Deploying the HTTP Function

To deploy the HTTP function to FunctionGraph, follow these steps:

Build deployment package

Following command will create a deployment package in ZIP format:

dotnet publish

Deploy the function to FunctionGraph

Use OpentelekomCloud FunctionGraph console to create a function with following settings:

Create function

Create With: Create from scratch

Basic Information

  • Function Type HTTP Function

  • Region <YOUR REGION>

  • Function Name <YOUR FUNCTION NAME>

Upload code

Use Upload > Local ZIP and upload the generated ZIP file from step 1

($(MSBuildProjectName)_net8.0.zip).

Configure function

Create an API Gateway trigger for the function to expose it as an HTTP endpoint.

For details on creating an API Gateway trigger, see Using an API Gateway Trigger.

Configure the API Gateway settings:

  • Set the Security Authentication to: NONE

  • Set Protocol to: HTTPS.

  • Set Method to: ANY.

After creation URL of the API Gateway endpoint is displayed. This URL will be used as <api-gateway-endpoint> in following step.

Test the function

Using a browser

You can use this URL to invoke your HTTP function from a web browser using following endpoints:

https://<api-gateway-endpoint>/
https://<api-gateway-endpoint>/greeting/John
https://<api-gateway-endpoint>/hello?name=John
https://<api-gateway-endpoint>/test

Using Test in FunctionGraph console

Testing the “root” endpoint

Create a Test Event in Code section of the FunctionGraph console with following content:

{
    "body": "",
    "requestContext": {
        "apiId": "bc1dcffd-aa35-474d-897c-d53425a4c08e",
        "requestId": "11cdcdcf33949dc6d722640a13091c77",
        "stage": "RELEASE"
    },
    "queryStringParameters": {
        "responseType": "html"
    },
    "httpMethod": "GET",
    "pathParameters": {},
    "headers": {
        "accept-language": "q=0.5,en-US;q=0.3,en;q=0.2",
        "accept-encoding": "gzip, deflate, br",
        "x-forwarded-port": "443",
        "x-forwarded-for": "103.218.216.98",
        "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "upgrade-insecure-requests": "1",
        "host": "host",
        "x-forwarded-proto": "https",
        "pragma": "no-cache",
        "cache-control": "no-cache",
        "x-real-ip": "103.218.216.98",
        "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0"
    },
    "path": "/",
    "isBase64Encoded": true
}

Select this Test Event and click Test to test the function.

The function execution result is displayed in the Execution Result section.

{
    "body": "SGVsbG8gV29ybGQh",
    "headers": {
        "Content-Type": [
            "text/plain; charset=utf-8"
        ],
        "Date": [
            "Tue, 25 Nov 2025 12:20:11 GMT"
        ],
        "Server": [
            "Kestrel"
        ]
    },
    "statusCode": 200,
    "isBase64Encoded": true
}

The response body is Base64 encoded.

Decode the body to see the actual response:

echo "SGVsbG8gV29ybGQh" | base64 --decode

This displays:

Hello World!

Testing the “hello” endpoint

Create a Test Event in Code section of the FunctionGraph console with following content:

{
    "body": "",
    "requestContext": {
        "apiId": "bc1dcffd-aa35-474d-897c-d53425a4c08e",
        "requestId": "11cdcdcf33949dc6d722640a13091c77",
        "stage": "RELEASE"
    },
    "queryStringParameters": {
        "responseType": "html",
        "name": "John"
    },
    "httpMethod": "GET",
    "pathParameters": {},
    "headers": {
        "accept-language": "q=0.5,en-US;q=0.3,en;q=0.2",
        "accept-encoding": "gzip, deflate, br",
        "x-forwarded-port": "443",
        "x-forwarded-for": "103.218.216.98",
        "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "upgrade-insecure-requests": "1",
        "host": "host",
        "x-forwarded-proto": "https",
        "pragma": "no-cache",
        "cache-control": "no-cache",
        "x-real-ip": "103.218.216.98",
        "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0"
    },
    "path": "/hello",
    "isBase64Encoded": true
}

Select this Test Event and click Test to test the function.

The function execution result is displayed in the Execution Result section.

{
    "body": "SGVsbG8sIEpvaG4h",
    "headers": {
        "Content-Type": [
            "text/plain; charset=utf-8"
        ],
        "Date": [
            "Tue, 25 Nov 2025 12:31:19 GMT"
        ],
        "Server": [
            "Kestrel"
        ]
    },
    "statusCode": 200,
    "isBase64Encoded": true
}

The response body is Base64 encoded.

Decode the body to see the actual response:

echo "SGVsbG8sIEpvaG4h" | base64 --decode

This displays:

Hello, John!