SAP S/4HANA Cloud and Microsoft .NET 6: Deployment on SAP BTP


Motivation

In a customer meeting this week I was asked: “We develop Microsoft .NET applications. Can we run them on SAP BTP?”.

I found this a fascinating challenge – particularly since I never worked with MS .NET as a developer before. Why not try and see if and how it would work? Let’s go!

Proof of concept scenario

In this blog we develop and deploy a .NET Blazor application on SAP BTP Kyma which is SAP’s Kubernetes runtime. I decided to connect to S/4HANA Cloud, retrieve some data and display it in the application to prove integration to SAP solutions, too.

Image%201%3A%20High%20level%20architecture%20of%20.NET%20deployment

Image 1: High level architecture of .NET deployment

Development

As said, I’m not well-versed on .NET and I didn’t want to install Visual Studio. I used VSCode with the C# extension of Omnisharp installed. That works very well for me. Next I downloaded .NET 6.0 SDK from Microsoft.

I then followed the tutorial of building a Blazor application. Finally I added an additional page to the app to load and display data from S/4HANA Cloud.

For those of you like me that never worked with C# here’s my masterpiece of integration. 😅 If we have an experienced C#/ Blazor developer among the readers I would love to hear how to avoid the interim step to remove the unwanted JSON hierarchy. Also I didn’t manage to convert the JSON date into a DateTime variable. For that there are JsonConverters in .NET. But I didn’t make it work.

@code { private IEnumerable<SupplierInvoices> invoices = Array.Empty<SupplierInvoices>(); private bool getInvoicesError; private bool shouldRender; protected override bool ShouldRender() => shouldRender; protected override async Task OnInitializedAsync() { var request = new HttpRequestMessage(HttpMethod.Get,"https://<url of service>"); request.Headers.Add("Accept", "application/json"); request.Headers.Add("User-Agent", "Blazor-http-client"); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("<username>:<password>"))); var client = ClientFactory.CreateClient(); var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) { using var responseStream = await response.Content.ReadAsStreamAsync(); JsonNode odataSet = await JsonSerializer.DeserializeAsync<JsonNode>(responseStream); var jsonString = odataSet["d"]["results"].ToString(); invoices = JsonSerializer.Deserialize<IEnumerable<SupplierInvoices>>(jsonString); } else { getInvoicesError = true; } shouldRender = true; } public class SupplierInvoices { [JsonPropertyName("SupplierInvoice")] public string? SupplierInvoice { get; set; } [JsonPropertyName("FiscalYear")] public string? FiscalYear { get; set; } [JsonPropertyName("CompanyCode")] public string? CompanyCode { get; set; } [JsonPropertyName("AccountingDocumentType")] public string? AccountingDocumentType { get; set; } [JsonPropertyName("InvoicingParty")] public string? InvoicingParty { get; set; } [JsonPropertyName("DocumentCurrency")] public string? DocumentCurrency { get; set; } [JsonPropertyName("InvoiceGrossAmount")] public string? InvoiceGrossAmount { get; set; } [JsonPropertyName("DocumentHeaderText")] public string? DocumentHeaderText { get; set; } [JsonPropertyName("PaymentTerms")] public string? PaymentTerms { get; set; } [JsonPropertyName("DueCalculationBaseDate")] public string? DueCalculationBaseDate { get; set; } }
}

With that code the invoice data is moved to invoices and can be displayed on the page in such a way:

@if (getInvoicesError)
{ <p>Unable to get invoices from S/4HANA Cloud. Sorry for that.</p>
}
else
{ <table class="table"> <thead> <tr> <th>Invoice #</th> <th>Document type</th> <th>Fiscal year</th> <th>Invoicing party</th> <th>Gross amount</th> <th>Company code</th> <th>Header text</th> <th>Payment terms</th> </tr> </thead> <tbody> @foreach (var invoice in invoices) { <tr> <td>@invoice.SupplierInvoice</td> <td>@invoice.AccountingDocumentType</td> <td>@invoice.FiscalYear</td> <td>@invoice.InvoicingParty</td> <td>@invoice.DocumentCurrency @invoice.InvoiceGrossAmount</td> <td>@invoice.CompanyCode</td> <td>@invoice.DocumentHeaderText</td> <td>@invoice.PaymentTerms</td> </tr> } </tbody> </table>
}

Now that we are through with the code, next step is deployment of the application.

Deployment

Deployment happens in two steps: First, build a docker image, second deploy on SAP BTP Kyma.

1. Docker image creation

Make sure in your Program.cs you have commented or removed the line

app.UseHttpsRedirection();

We don’t want the application to use https since the security is completely handled by Kyma’s API Rules. As you might know communication among the Kubernetes pods is always http (unless we go a level deeper and work with sockets).

Microsoft publishes official Docker images which you can find here. As for the Dockerfile it looks for me like below.

# Create docker image to be used on SAP BTP Kyma for .NET web server
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
# Port number of server
EXPOSE 80 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /usr/src COPY ["BlazorApp.csproj", "."]
RUN dotnet restore "BlazorApp.csproj"
# Copy the code into the workdir
COPY . .
RUN dotnet build "BlazorApp.csproj" -c Release -o /usr/app/build FROM build AS publish
RUN dotnet publish "BlazorApp.csproj" -c Release -o /usr/app/publish FROM base AS final
WORKDIR /usr/app
COPY --from=publish /usr/app/publish .
ENTRYPOINT ["dotnet", "BlazorApp.dll"]

You need to adjust for your application name. Run the usual docker build like so:

docker build --tag <your docker id>/<your image name>:<your tag version> .

and push it to the docker hub with this command:

docker push <your docker id>/<your image name>:<your tag version>

Now we are good to deploy on SAP BTP Kyma.

2. Kyma deployment

That part is easy, we need the usual deployment yaml which you can copy from below. Adjust it to your namespace and hostname and don’t forget to update the image name.

apiVersion: v1
kind: Namespace
metadata: name: dotnet-blazor labels: istio-injection: enabled
---
apiVersion: apps/v1
kind: Deployment
metadata: name: dotnet-blazor namespace: dotnet-blazor
spec: replicas: 1 selector: matchLabels: app: dotnet-blazor-app template: metadata: labels: app: dotnet-blazor-app spec: containers: - name: dotnet-blazor-image image: <your docker id>/<your image name>:<your tag version> imagePullPolicy: Always ports: - containerPort: 80 resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m"
---
apiVersion: v1
kind: Service
metadata: labels: app: dotnet-blazor-app name: dotnet-blazor-service namespace: dotnet-blazor
spec: ports: - name: "webserverport" port: 8080 targetPort: 80 type: ClusterIP selector: app: dotnet-blazor-app
--
apiVersion: gateway.kyma-project.io/v1alpha1
kind: APIRule
metadata: labels: app.kubernetes.io/name: dotnet-blazor-apirule name: dotnet-blazor-apirule namespace: dotnet-blazor
spec: gateway: kyma-gateway.kyma-system.svc.cluster.local rules: - accessStrategies: - config: {} handler: allow methods: - GET - POST - PUT - DELETE - PATCH - HEAD path: /.* service: host: myblazorapp name: dotnet-blazor-service port: 8080 

And with that we are good to test it:

Image%202%3A%20Walk%20through%20the%20running%20application

Image 2: Walk through the running application

Closing

And here it ends, our little excursion. While this blog was focussing on front-end application development it runs a .NET web server in the backend. Certainly there should be no concerns to integrate with SAP or Non-SAP applications when developing with Microsoft .NET on SAP BTP. Hope it was useful or interesting and would be happy to hear your feedback.