Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
Gunter
Product and Topic Expert
Product and Topic Expert


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 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 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.