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:
/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,
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
Navigate to the project directory:
cd http_minimalWebAPI
Replace the contents of the Program.cs file with the code of next section.
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¶
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!