Linux container for the .NET Framework application (when it is difficult to leave for .Net Core)

Hello, Habr.

I want to share with the world a rather atypical task, at least for me, and its solution, which seems to me quite acceptable. Described below may not be the ideal way out of the situation, but it works, and works as intended.

Setting and background


There was a task for work: you need to make 3D previews of BIM models of various equipment, materials, objects on the site. Need something lightweight, uncomplicated.

On the website, models of these objects are stored and available for download in proprietary formats of various CAD systems and in the form of open formats for 3D models. Among them is the IFC format . I will use it as a source for solving this task.

One of the options and its features


Formally, one could restrict oneself to writing some kind of * .ifc converter for something to display on a web page. This is where I started.

A wonderful toolkit, the xBIM Toolkit, was chosen for such a conversion .

The examples of using this tool simply and clearly describe how to work with IFC and the * .wexBIM specialized for the web-format.

First, convert * .ifc to * .wexBIM:
using System.IO;
using Xbim.Ifc;
using Xbim.ModelGeometry.Scene;

namespace CreateWexBIM
{
    class Program
    {
        public static void Main()
        {
            const string fileName = "SampleHouse.ifc";
            using (var model = IfcStore.Open(fileName))
            {
                var context = new Xbim3DModelContext(model);
                context.CreateContext();

                var wexBimFilename = Path.ChangeExtension(fileName, "wexBIM");
                using (var wexBiMfile = File.Create(wexBimFilename))
                {
                    using (var wexBimBinaryWriter = new BinaryWriter(wexBiMfile))
                    {
                        model.SaveAsWexBim(wexBimBinaryWriter);
                        wexBimBinaryWriter.Close();
                    }
                    wexBiMfile.Close();
                }
            }
        }
    }
}


Next, the resulting file is used in the "player" xBIM WeXplorer .

An example of embedding * .wexBIM in a page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Hello building!</title>
    <script src="js/xbim-viewer.debug.bundle.js"></script>
</head>
<body>
    <div id="content">
        <canvas id="viewer" width="500" height="300"></canvas>
        <script type="text/javascript">
            var viewer = new xViewer('viewer');
            viewer.load('data/SampleHouse.wexbim');
            viewer.start();
        </script>
    </div>    
</body>
</html>


Well, let's go. I take nugets from xBIM. I am writing a console application that receives a bunch of paths to * .ifc files as input, and next to them it adds a bunch of * .wexBIM files. Everything can be uploaded to the site.

But somehow it’s quite simple ... I want this program to become a kind of service, which, upon the * .ifc upload event to the portal, immediately creates the necessary * .wexBIM, and it immediately appears in the prepared container.

Ok, I form new requirements:

  1. Let conversion tasks come from our RabbitMQ ;
  2. I want to see the tasks themselves in the form of a binary message, which in fact will be ready for deserialization by the class described in the protobuf file;
  3. the task will contain a link to download the source * .ifc file from our Minio ;
  4. the task will also tell me which bucket in Minio to add the result;
  5. let the application itself be built under .net core 3.1 and work inside the Linux docker container on our "docker farm";

The first difficulties and conventions


I will not describe in detail the first 4 points of implementation. Maybe later.

Caused the application to listen to the job queue and send a message with the result to the queue from the CorrelationId of the job message. Screwed the generated request / response classes from protobuf. Taught to download / upload files in minio .

All this I do in the console application project. In the project settings:

<TargetFramework>netcoreapp3.1</TargetFramework>

And on my machine with Windows 10 everything is completely debugged and works. But when I try to run the application in WSL, I catch a System.IO.FileLoadException error :

Full error information:
{
  "Type": "System.IO.FileLoadException",
  "Message": "Failed to load Xbim.Geometry.Engine64.dll",
  "TargetSite": "Void .ctor(Microsoft.Extensions.Logging.ILogger`1[Xbim.Geometry.Engine.Interop.XbimGeometryEngine])",
  "StackTrace": " at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor(ILogger`1 logger)\r\n at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor()\r\n at Xbim.ModelGeometry.Scene.Xbim3DModelContext.get_Engine()\r\n at Xbim.ModelGeometry.Scene.Xbim3DModelContext.CreateContext(ReportProgressDelegate progDelegate, Boolean adjustWcs)\r\n at My.Converter.ConvertIfc.CreateWebIfc(String ifcFileFullPath, String wexBIMFolder)",
  "Data": {},
  "InnerException": {
    "Type": "System.IO.FileNotFoundException",
    "Message": "Could not load file or assembly 'Xbim.Geometry.Engine.dll, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.",
    "FileName": "Xbim.Geometry.Engine.dll, Culture=neutral, PublicKeyToken=null",
    "FusionLog": "",
    "TargetSite": "System.Reflection.RuntimeAssembly nLoad(System.Reflection.AssemblyName, System.String, System.Reflection.RuntimeAssembly, System.Threading.StackCrawlMark ByRef, Boolean, System.Runtime.Loader.AssemblyLoadContext)",
    "StackTrace": " at System.Reflection.RuntimeAssembly.nLoad(AssemblyName fileName, String codeBase, RuntimeAssembly assemblyContext, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, AssemblyLoadContext assemblyLoadContext)\r\n at System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, StackCrawlMark& stackMark, AssemblyLoadContext assemblyLoadContext)\r\n at System.Reflection.Assembly.Load(String assemblyString)\r\n at Xbim.Geometry.Engine.Interop.XbimGeometryEngine..ctor(ILogger`1 logger)",
    "Data": {},
    "Source": "System.Private.CoreLib",
    "HResult": -2147024894
  },
  "Source": "Xbim.Geometry.Engine.Interop",
  "HResult": -2146232799
}

A session of active googling and thoughtful reading showed me that I was extremely inattentive:
Recently at work, we were evaluating a few options to render building models in the browser. Building Information Modeling (BIM) in interoperability scenarios is done via Industry Foundation Classes, mostly in the STEP Physical File format. The schema is quite huge and complex with all the things you have to consider, so we were glad to find the xBim open source project on GitHub. They've got both projects to visualize building models in the browser with WebGL as well as conversion tools to create the binary-formatted geometry mesh. To achieve that, native C++ libraries are dynamically loaded (so no .Net Core compatibility) which must be present in the bin folder. The C++ libraries are expected either in the same folder as the application binaries or in a x64 (or x86, respectively) sub folder (See here for more details). In regular projects, the xBim.Geometry NuGet package adds a build task to copy the dlls into the build output folder, but this doesn't work with the new tooling. You can, however, get it to work in Visual Studio 2015 by taking care of supplying the interop dlls yourself.

And similar difficulties are not for me alone. Many people want xBIM under .Net Core .
Not critical, but it changes a lot ... Everything depends on the inability to load Xbim.Geometry.Engine64.dll normally . You need to have vc_redist.x64.exe on the machine . What are my options?
The first thing I thought: β€œCan a Windows container with a full .Net Framework be used?
Deliver Microsoft Visual C ++ Redistributable for Visual Studio 2015, 2017 and 2019 to this container, and everything will be ok? ” I tried this:

Test Windows image for docker:
.Net Core :

<TargetFramework>net47</TargetFramework>

Dockerfile:

FROM microsoft/dotnet-framework:4.7
WORKDIR /bimlibconverter
COPY lib/VC_redist.x64.exe /VC_redist.x64.exe
RUN C:\VC_redist.x64.exe /quiet /install
COPY bin/Release .
ENTRYPOINT ["MyConverter.exe"]

Well, it worked ... It's alive! But. But what about our host Linux machine with docker? It will not work to drive a container with an image on Windows Server Core onto it . Need to get out ...

Compromise and denouement


Another search on the Web brought me to an article . In it, the author requires a similar implementation:
To make things worse:
All binaries are 32-bits (x86).
Some require visual C ++ redistributable runtime components.
Some require the .NET runtime.
Some need a windowing system, even though we only use the command-line interface (CLI).
The post describes the potential for running Windows applications in wine in a Linux container. Curious, I decided.

After some tests, bugs and additions, the Dockerfile was received:

Ubuntu-based docker image with Wine, .Net Framework and vcredist on board:

FROM ubuntu:latest
#  x86
RUN dpkg --add-architecture i386 \
    && apt-get update \
    #   
    && apt-get install -qfy --install-recommends \
        software-properties-common \
        gnupg2 \
        wget \
        xvfb \
        cabextract \
    #  Wine
    && wget -nv https://dl.winehq.org/wine-builds/winehq.key \
    && apt-key add winehq.key \
    && apt-add-repository 'deb https://dl.winehq.org/wine-builds/ubuntu/ bionic main' \
    #     Wine
    && add-apt-repository ppa:cybermax-dexter/sdl2-backport \
    #  Wine
    && apt-get install -qfy --install-recommends \
        winehq-staging \
        winbind \
    # 
    && apt-get -y clean \
    && rm -rf \
      /var/lib/apt/lists/* \
      /usr/share/doc \
      /usr/share/doc-base \
      /usr/share/man \
      /usr/share/locale \
      /usr/share/zoneinfo
#    Wine
ENV WINEDEBUG=fixme-all
ENV WINEPREFIX=/root/.net
ENV WINEARCH=win64
#  Wine
RUN winecfg \
    # winetricks,   .Net Framework  
    && wget https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks \
    -O /usr/local/bin/winetricks \
    && chmod +x /usr/local/bin/winetricks \
# 
    && apt-get -y clean \
    && rm -rf \
      /var/lib/apt/lists/* \
      /usr/share/doc \
      /usr/share/doc-base \
      /usr/share/man \
      /usr/share/locale \
      /usr/share/zoneinfo \
    # Wine   
    && wineboot -u && winetricks -q dotnet472 && xvfb-run winetricks -q vcrun2015

WORKDIR /root/.net/drive_c/myconverter/

#  
COPY /bin/Release/ /root/.net/drive_c/myconverter/

ENTRYPOINT ["wine", "MyConverter.exe"]

UPD: . rueler

Build is not fast, but ends successfully. I try, check. Works!

Results, conclusions, thoughts


It worked. The output is a Linux image for the docker container. It is β€œpuffy” (~ 5.2GB), but it starts quite quickly and inside it runs a Windows console application on .Net Framework 4.7, which listens to RabbitMQ, writes logs to Graylog , downloads and uploads files to / in Minio. I will update the application by the remote docker API.

The solution to the utilitarian dedicated task is implemented. Perhaps, and most likely, not universal. But in principle I was satisfied. Maybe someone will come in handy too.

Thanks for reading. I write on Habr for the first time. See you in the comments.

All Articles