Browse Source

Upgrade appMpower to .NET 9 and use Razor slices (#9398)

* Dotnet 9.0

* Using razor slices

* System.Data.Odbc version 9.0.0

* Return to System.Data.Odbc because version 9 misses file libodbc.so

* Solution for System.Data.Odbc version 9.0.0 ?

---------

Co-authored-by: LLT21 <>
LLT21 9 months ago
parent
commit
e91044219c

+ 5 - 3
frameworks/CSharp/appmpower/appmpower-odbc-my.dockerfile

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0.100 AS build
+FROM mcr.microsoft.com/dotnet/sdk:9.0.100 AS build
 RUN apt-get update
 RUN apt-get update
 RUN apt-get -yqq install clang zlib1g-dev
 RUN apt-get -yqq install clang zlib1g-dev
 RUN apt-get update
 RUN apt-get update
@@ -8,12 +8,12 @@ COPY src .
 RUN dotnet publish -c Release -o out /p:Database=mysql
 RUN dotnet publish -c Release -o out /p:Database=mysql
 
 
 # Construct the actual image that will run
 # Construct the actual image that will run
-FROM mcr.microsoft.com/dotnet/aspnet:8.0.0 AS runtime
+FROM mcr.microsoft.com/dotnet/aspnet:9.0.0 AS runtime
 
 
 RUN apt-get update
 RUN apt-get update
 # The following installs standard versions unixodbc and pgsqlodbc
 # The following installs standard versions unixodbc and pgsqlodbc
 # unixodbc still needs to be installed even if compiled locally
 # unixodbc still needs to be installed even if compiled locally
-RUN apt-get install -y unixodbc wget curl
+RUN apt-get install -y unixodbc-dev unixodbc wget curl
 RUN apt-get update
 RUN apt-get update
 
 
 WORKDIR /odbc
 WORKDIR /odbc
@@ -45,6 +45,8 @@ WORKDIR /app
 COPY --from=build /app/out ./
 COPY --from=build /app/out ./
 
 
 RUN cp /usr/lib/libm* /app
 RUN cp /usr/lib/libm* /app
+#RUN cp /usr/lib/aarch64-linux-gnu/libodbc* /app
+RUN cp /usr/lib/x86_64-linux-gnu/libodbc* /app
 
 
 EXPOSE 8080
 EXPOSE 8080
 
 

+ 7 - 3
frameworks/CSharp/appmpower/appmpower-odbc-pg.dockerfile

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0.100 AS build
+FROM mcr.microsoft.com/dotnet/sdk:9.0.100 AS build
 RUN apt-get update
 RUN apt-get update
 RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5
 RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5
 
 
@@ -7,10 +7,10 @@ COPY src .
 RUN dotnet publish -c Release -o out /p:Database=postgresql
 RUN dotnet publish -c Release -o out /p:Database=postgresql
 
 
 # Construct the actual image that will run
 # Construct the actual image that will run
-FROM mcr.microsoft.com/dotnet/aspnet:8.0.0 AS runtime
+FROM mcr.microsoft.com/dotnet/aspnet:9.0.0 AS runtime
 
 
 RUN apt-get update
 RUN apt-get update
-RUN apt-get install -y unixodbc odbc-postgresql
+RUN apt-get install -y unixodbc-dev unixodbc odbc-postgresql
 # unixodbc still needs to be installed even if compiled locally
 # unixodbc still needs to be installed even if compiled locally
 
 
 ENV PATH=/usr/local/unixODBC/bin:$PATH
 ENV PATH=/usr/local/unixODBC/bin:$PATH
@@ -27,6 +27,10 @@ ENV ASPNETCORE_URLS http://+:8080
 WORKDIR /app
 WORKDIR /app
 COPY --from=build /app/out ./
 COPY --from=build /app/out ./
 
 
+#RUN cp /usr/lib/aarch64-linux-gnu/libodbc* /app
+RUN cp /usr/lib/x86_64-linux-gnu/libodbc* /app
+
+
 EXPOSE 8080
 EXPOSE 8080
 
 
 ENTRYPOINT ["./appMpower"]
 ENTRYPOINT ["./appMpower"]

+ 2 - 2
frameworks/CSharp/appmpower/appmpower.dockerfile

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0.100 AS build
+FROM mcr.microsoft.com/dotnet/sdk:9.0.100 AS build
 RUN apt-get update
 RUN apt-get update
 RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5
 RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5
 
 
@@ -8,7 +8,7 @@ COPY src .
 RUN dotnet publish -c Release -o out
 RUN dotnet publish -c Release -o out
 
 
 # Construct the actual image that will run
 # Construct the actual image that will run
-FROM mcr.microsoft.com/dotnet/aspnet:8.0.0 AS runtime
+FROM mcr.microsoft.com/dotnet/aspnet:9.0.0 AS runtime
 # Full PGO
 # Full PGO
 ENV DOTNET_TieredPGO 1 
 ENV DOTNET_TieredPGO 1 
 ENV DOTNET_TC_QuickJitForLoops 1 
 ENV DOTNET_TC_QuickJitForLoops 1 

+ 59 - 0
frameworks/CSharp/appmpower/src/appMpower.Orm/NativeMethods.cs

@@ -17,6 +17,9 @@ public static class NativeMethods
 
 
     private readonly static WorldSerializer _worldSerializer = new WorldSerializer();
     private readonly static WorldSerializer _worldSerializer = new WorldSerializer();
     private readonly static WorldsSerializer _worldsSerializer = new WorldsSerializer();
     private readonly static WorldsSerializer _worldsSerializer = new WorldsSerializer();
+    private readonly static FortunesSerializer _fortunesSerializer = new FortunesSerializer();
+    private static readonly byte[] _delimiter = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
+
 
 
     [UnmanagedCallersOnly(EntryPoint = "Dbms")]
     [UnmanagedCallersOnly(EntryPoint = "Dbms")]
     public static void Dbms(int dbms)
     public static void Dbms(int dbms)
@@ -66,6 +69,7 @@ public static class NativeMethods
         */
         */
     }
     }
 
 
+    /*
     [UnmanagedCallersOnly(EntryPoint = "Fortunes")]
     [UnmanagedCallersOnly(EntryPoint = "Fortunes")]
     public static unsafe IntPtr Fortunes(int* length, IntPtr* handlePointer)
     public static unsafe IntPtr Fortunes(int* length, IntPtr* handlePointer)
     {
     {
@@ -81,6 +85,61 @@ public static class NativeMethods
 
 
         return byteArrayPointer;
         return byteArrayPointer;
     }
     }
+    */
+
+    [UnmanagedCallersOnly(EntryPoint = "Fortunes")]
+    public static unsafe IntPtr Fortunes(int* length, IntPtr* handlePointer)
+    {
+        List<Fortune> fortunes = RawDb.LoadFortunesRows().GetAwaiter().GetResult(); 
+
+        int totalSize = 0;
+
+        foreach (var fortune in fortunes)
+        {
+            totalSize += sizeof(int) // for Id
+                       + Encoding.UTF8.GetByteCount(fortune.Message ?? "") // for Message
+                       + _delimiter.Length; // for delimiter
+        }
+
+        // Allocate the total buffer
+        byte[] buffer = new byte[totalSize];
+        int offset = 0;
+
+        // Write each object to the buffer
+        foreach (var fortune in fortunes)
+        {
+            // Write Id
+            BitConverter.TryWriteBytes(buffer.AsSpan(offset, sizeof(int)), fortune.Id);
+            offset += sizeof(int);
+
+            // Write Message
+            int descriptionLength = Encoding.UTF8.GetBytes(fortune.Message ?? "", buffer.AsSpan(offset));
+            offset += descriptionLength;
+
+            // Write Delimiter
+            _delimiter.CopyTo(buffer, offset);
+            offset += _delimiter.Length;
+        }
+
+        byte[] byteArray = buffer.ToArray();
+        *length = byteArray.Length; 
+
+        /*
+        var memoryStream = new MemoryStream();
+        using var utf8JsonWriter = new Utf8JsonWriter(memoryStream, _jsonWriterOptions);
+
+        _fortunesSerializer.Serialize(utf8JsonWriter, fortunes);
+
+        byte[] byteArray = memoryStream.ToArray();
+        *length = (int)utf8JsonWriter.BytesCommitted; 
+        */
+
+        GCHandle handle = GCHandle.Alloc(byteArray, GCHandleType.Pinned);
+        IntPtr byteArrayPointer = handle.AddrOfPinnedObject();
+        *handlePointer = GCHandle.ToIntPtr(handle);
+
+        return byteArrayPointer;
+    }
 
 
     [UnmanagedCallersOnly(EntryPoint = "Query")]
     [UnmanagedCallersOnly(EntryPoint = "Query")]
     public static unsafe IntPtr Query(int queries, int* length, IntPtr* handlePointer)
     public static unsafe IntPtr Query(int queries, int* length, IntPtr* handlePointer)

+ 24 - 0
frameworks/CSharp/appmpower/src/appMpower.Orm/Serializers/FortunesSerializer.cs

@@ -0,0 +1,24 @@
+using System.Text.Json;
+using appMpower.Orm.Objects;
+
+namespace appMpower.Orm.Serializers
+{
+   public class FortunesSerializer : IJsonSerializer<List<Fortune>>
+   {
+      public void Serialize(Utf8JsonWriter utf8JsonWriter, List<Fortune> fortunes)
+      {
+         utf8JsonWriter.WriteStartArray();
+
+         foreach (Fortune fortune in fortunes)
+         {
+            utf8JsonWriter.WriteStartObject();
+            utf8JsonWriter.WriteNumber("id", fortune.Id);
+            utf8JsonWriter.WriteString("message", fortune.Message);
+            utf8JsonWriter.WriteEndObject();
+         }
+
+         utf8JsonWriter.WriteEndArray();
+         utf8JsonWriter.Flush();
+      }
+   }
+}

+ 2 - 2
frameworks/CSharp/appmpower/src/appMpower.Orm/appMpower.Orm.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <ImplicitUsings>enable</ImplicitUsings>
 
 
     <PublishAot>true</PublishAot>
     <PublishAot>true</PublishAot>
@@ -36,7 +36,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="System.Data.Odbc" Version="8.0.0" />
+    <PackageReference Include="System.Data.Odbc" Version="9.0.0" />
   </ItemGroup>
   </ItemGroup>
 
 
 </Project>
 </Project>

+ 115 - 0
frameworks/CSharp/appmpower/src/appMpower/Middleware/FortunesMiddleware.cs

@@ -2,7 +2,11 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using System.Text;
 using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Unicode;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using appMpower.Objects;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Primitives;
 using Microsoft.Extensions.Primitives;
@@ -11,11 +15,21 @@ namespace appMpower;
 
 
 public class FortunesMiddleware
 public class FortunesMiddleware
 {
 {
+    static readonly HtmlEncoder htmlEncoder = CreateHtmlEncoder();
+    static HtmlEncoder CreateHtmlEncoder()
+    {
+        var settings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Katakana, UnicodeRanges.Hiragana);
+        settings.AllowCharacter('\u2014'); // allow EM DASH through
+        return HtmlEncoder.Create(settings);
+    }
+
     private readonly static KeyValuePair<string, StringValues> _headerServer =
     private readonly static KeyValuePair<string, StringValues> _headerServer =
          new KeyValuePair<string, StringValues>("Server", new StringValues("k"));
          new KeyValuePair<string, StringValues>("Server", new StringValues("k"));
     private readonly static KeyValuePair<string, StringValues> _headerContentType =
     private readonly static KeyValuePair<string, StringValues> _headerContentType =
          new KeyValuePair<string, StringValues>("Content-Type", new StringValues("text/html; charset=UTF-8"));
          new KeyValuePair<string, StringValues>("Content-Type", new StringValues("text/html; charset=UTF-8"));
 
 
+    private static readonly byte[] _delimiter = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
+
     private readonly RequestDelegate _next;
     private readonly RequestDelegate _next;
 
 
     public FortunesMiddleware(RequestDelegate next)
     public FortunesMiddleware(RequestDelegate next)
@@ -27,6 +41,66 @@ public class FortunesMiddleware
     {
     {
         if (httpContext.Request.Path.StartsWithSegments("/fortunes", StringComparison.Ordinal))
         if (httpContext.Request.Path.StartsWithSegments("/fortunes", StringComparison.Ordinal))
         {
         {
+            int payloadLength;
+            IntPtr handlePointer; 
+
+            IntPtr bytePointer = NativeMethods.Fortunes(out payloadLength, out handlePointer);
+
+            /*
+            byte[] json = new byte[payloadLength];
+            Marshal.Copy(bytePointer, json, 0, payloadLength);
+            NativeMethods.FreeHandlePointer(handlePointer);
+
+            string s = Encoding.UTF8.GetString(json, 0, json.Length);
+
+            var options = new JsonSerializerOptions
+            {
+                PropertyNameCaseInsensitive = true
+            };
+
+            List<Fortune> fortunes = JsonSerializer.Deserialize<List<Fortune>>(s, options);
+
+            var response = httpContext.Response; 
+            response.Headers.Add(_headerServer);
+
+            var result = Results.Extensions.RazorSlice<Slices.Fortunes, List<Fortune>>(fortunes);
+            result.HtmlEncoder = htmlEncoder;
+
+            return result.ExecuteAsync(httpContext);
+            */
+
+            byte[] byteArray = new byte[payloadLength];
+            Marshal.Copy(bytePointer, byteArray, 0, payloadLength);
+
+            List<Fortune> fortunes = new List<Fortune>();
+
+            // Convert the byte array into segments split by the delimiter
+            int delimiterLength = _delimiter.Length;
+            int start = 0;
+            int index;
+
+            while ((index = FindDelimiterIndex(byteArray, _delimiter, start)) >= 0)
+            {
+                // Use a span over the segment of bytes for the current object
+                var objectDataSpan = new ReadOnlySpan<byte>(byteArray, start, index - start);
+                Fortune fortune = ConvertBytesToObject(objectDataSpan);
+                fortunes.Add(fortune);
+
+                // Move past the delimiter
+                start = index + delimiterLength;
+            }
+
+            NativeMethods.FreeHandlePointer(handlePointer);
+
+            var response = httpContext.Response; 
+            response.Headers.Add(_headerServer);
+
+            var result = Results.Extensions.RazorSlice<Slices.Fortunes, List<Fortune>>(fortunes);
+            result.HtmlEncoder = htmlEncoder;
+
+            return result.ExecuteAsync(httpContext);
+
+            /*
             var response = httpContext.Response; 
             var response = httpContext.Response; 
             response.Headers.Add(_headerServer);
             response.Headers.Add(_headerServer);
             response.Headers.Add(_headerContentType);
             response.Headers.Add(_headerContentType);
@@ -43,10 +117,51 @@ public class FortunesMiddleware
                 new KeyValuePair<string, StringValues>("Content-Length", payloadLength.ToString()));
                 new KeyValuePair<string, StringValues>("Content-Length", payloadLength.ToString()));
 
 
             return response.Body.WriteAsync(json, 0, payloadLength);
             return response.Body.WriteAsync(json, 0, payloadLength);
+            */
         }
         }
 
 
         return _next(httpContext);
         return _next(httpContext);
     }
     }
+
+    private static int FindDelimiterIndex(byte[] array, byte[] delimiter, int startIndex)
+    {
+        int endIndex = array.Length - delimiter.Length;
+
+        for (int i = startIndex; i <= endIndex; i++)
+        {
+            bool isMatch = true;
+
+            for (int j = 0; j < delimiter.Length; j++)
+            {
+                if (array[i + j] != delimiter[j])
+                {
+                    isMatch = false;
+                    break;
+                }
+            }
+
+            if (isMatch)
+            {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    private static Fortune ConvertBytesToObject(ReadOnlySpan<byte> data)
+    {
+        int offset = 0;
+
+        // Read Id
+        int id = BitConverter.ToInt32(data.Slice(offset, sizeof(int)));
+        offset += sizeof(int);
+
+        // Read Message (remaining bytes in the span)
+        string message = Encoding.UTF8.GetString(data.Slice(offset));
+
+        return new Fortune(id, message);
+    }
 }
 }
 
 
 public static class FortunesMiddlewareExtensions
 public static class FortunesMiddlewareExtensions

+ 22 - 0
frameworks/CSharp/appmpower/src/appMpower/Objects/Fortune.cs

@@ -0,0 +1,22 @@
+using System;
+
+namespace appMpower.Objects
+{
+   public struct Fortune : IComparable<Fortune>, IComparable
+   {
+      public Fortune(int id, string message)
+      {
+         Id = id;
+         Message = message;
+      }
+
+      public int Id { get; set; }
+
+      public string Message { get; set; }
+
+      public int CompareTo(object obj) => throw new InvalidOperationException("The non-generic CompareTo should not be used");
+
+      // Performance critical, using culture insensitive comparison
+      public int CompareTo(Fortune other) => string.CompareOrdinal(Message, other.Message);
+   }
+}

+ 2 - 0
frameworks/CSharp/appmpower/src/appMpower/Slices/Fortunes.cshtml

@@ -0,0 +1,2 @@
+@inherits RazorSliceHttpResult<List<appMpower.Objects.Fortune>>
+<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>@foreach (var item in Model){<tr><td>@WriteNumber(item.Id, default, CultureInfo.InvariantCulture, false)</td><td>@item.Message</td></tr>}</table></body></html>

+ 10 - 0
frameworks/CSharp/appmpower/src/appMpower/Slices/_ViewImports.cshtml

@@ -0,0 +1,10 @@
+@inherits RazorSliceHttpResult
+
+@using System.Globalization;
+@using Microsoft.AspNetCore.Razor;
+@using Microsoft.AspNetCore.Http.HttpResults;
+@using RazorSlices;
+@using appMpower.Objects;
+
+@tagHelperPrefix __disable_tagHelpers__:
+@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor

+ 2 - 1
frameworks/CSharp/appmpower/src/appMpower/appMpower.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <OutputType>Exe</OutputType>
     <OutputType>Exe</OutputType>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   </PropertyGroup>
@@ -24,6 +24,7 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include = "..\appMpower.Orm\appMpower.Orm.csproj" />
     <ProjectReference Include = "..\appMpower.Orm\appMpower.Orm.csproj" />
+    <PackageReference Include="RazorSlices" Version="0.8.1" />
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>