🥶

超レガシーな.NETをLinuxで動かして震え上がらせたい

に公開

はじめに

ちょっとまえにSNSでC#とLinuxについて論争がありましたが、.NET界隈からすると普通にLinuxで動くことを知っていたのでNETに携わっていない人たちとの認知の歪みはなぜだろう?と考えていました。

よくよく考えてみると.NETを知らない人たちがイメージしているのはこれだよなぁ。。。(以下の画像)
image.png

それならLinuxで動く認識なくてもしゃーないなと思ってました。

「昔は動かなかったけど今は動くよ」という結論に至ってしまったわけですが「いや、たしか昔からLinuxで動かせたような...今はDockerもあるし」と思い超レガシーな.NETをなんとしてでも動かせないかと検証してみました

超レガシーな技術を選定する

.NETのWEBフレームワークで代表的なものが以下となります。

  • ASP.NET Web Forms
  • ASP.NET MVC
  • ASP.NET Web API
  • Microsoft Silverlight

Web APIはRESTAPiのフレームワークなのでいわずもがなモダン技術と認定し除外します。MVCはいい線ですがマウント取れるほどのレガシー技術ではないので落選です。

Silverlight、ASP.NET Web Forms...

最高です。.NETを知らないひとが想像するのはまさにこれでしょうということでASP.NET Web FormsをLinuxで動かせるように奮闘します(Silverlightは知らなすぎるので断念しました)

ASP.NET Web Formsとは

.NET 最初期のWeb開発フレームワークで、2002年に登場した「サーバーサイドでGUI的にWebページを作る仕組み」です。Windows専用 (.NET Framework) で動き、いわば「WindowsフォームのWeb版」のような思想です。HTMLを直接書かず、サーバーコントロールを配置して、Button1_Click() のような サーバーイベントを C# コードビハインドで処理します。2000年代後半からRailsやAjaxなどのWEB技術が主流となり、.NETも後を追うように後継のASP.NET MVCを主力としたため、ASP.NET Web Formsは少なくなってきました。

流行りによってレガシーになったとはいえ、↓のような感じでUIを簡単に配置でき、簡単に高度なアプリケーションを作れる素晴らしいフレームワークとなっています
image.png

そもそも動かせるのか調べてみる

さっそく「asp.net web forms linux」でググってみます。

image.png

Nginx、Linux、ASP.NET Core。。。ぐぬぬ。。。Linuxで動く気配すら感じられません。。

つぎに「asp.net web forms docker」で調べてみます。おっ、そこそこ希望が持てるかも??

image.png

↑の質問に対する回答は0件でしたorz

調べてみるとASP.NET Web FormsはWindowsコンテナを利用すれば動く!Dockerで動くのであれば完璧!と思っていたらWindowsコンテナはLinux、Macで動かない。。。振り出しに戻ったのでもう少し奮闘...

なんとか動かせるっぽい

mod_monoというライブラリを利用すると動かせるようです。こいつを使ってなんとか動かしてみます。

https://www.mono-project.com/docs/web/mod_mono/

実はすごいMono

実はMono、ミゲル・デ・イカザというGNOMEを立ち上げた人が作っています。GNOMEとはubuntuの顔ともなっている超メジャーなデスクトップ環境です。ubuntuのデスクトップ環境は一瞬Unityになりましたがあまりにも不評すぎてGNOMEに戻った、それぐらいLinuxの顔ともなっている代表的なOSSです。そんな最強の人が作ったプロジェクトはXimian→Novell→Xamarin→Microsoftと渡り歩いてきて現在は.NETの共同開発、ソース提供など昨今の.NETにめちゃくちゃ貢献してる縁の下の力持ち的な存在となっています。Monoがなかったら今の.NETはなかったかもしれない

https://www.mono-project.com/docs/about-mono/dotnet-integration/?utm_source=chatgpt.com

うごかしてみる

私はMacを利用しているのでDockerで簡単なデモが動くか検証します

アプリケーション構成

ディレクトリ構成は以下のように単一ページの構成にします

├─ Dockerfile
└─ app/
   ├─ Default.aspx    // 表示(HTML)の定義
   |--Default.aspx.cs // ロジック(C#)の定義
   └─ Web.config      // 設定ファイル

ボタンクリックしたら「PostBack OK」と表示されるようにします

  • HTML的なやつ
Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs"
Inherits="_Default" %>
<!DOCTYPE html>
<html>
  <head runat="server">
    <meta charset="utf-8" />
    <title>Mono WebForms CodeBehind</title>
  </head>
  <body>
    <form id="form1" runat="server">
      <h1>Mono + Web Forms (CodeBehind)</h1>
      <asp:Label
        ID="Label1"
        runat="server"
        Text="Hello from CodeBehind!"
      ></asp:Label>
      <br />
      <asp:Button ID="Btn" runat="server" Text="PostBack" OnClick="Btn_Click" />
    </form>
  </body>
</html>
  • ソースコード
Default.aspx.cs
using System;

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e) { }

    protected void Btn_Click(object sender, EventArgs e)
    {
        Label1.Text = "PostBack OK: " + DateTime.Now.ToString("u");
    }
}

  • 設定ファイル
web.config
<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.7.2" />
    <pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID" />
    <globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8" culture="ja-JP" uiCulture="ja-JP"/>
  </system.web>
</configuration>

Dockerfileは最低限動けばよいのでシンプルにしています

Dockerfile
FROM debian:bullseye-slim

ARG DEBIAN_FRONTEND=noninteractive

# Reduce layers and avoid unnecessary packages
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends --no-install-suggests \
      apache2 \
      libapache2-mod-mono \
      mono-apache-server4 \
    ; \
    echo 'Listen 8080' > /etc/apache2/ports.conf; \
    a2enmod mono || true; \
    printf 'ServerName localhost\n' > /etc/apache2/conf-available/servername.conf; \
    a2enconf servername; \
    a2dismod mpm_event || true; \
    a2enmod mpm_prefork || true; \
    printf '%s\n' \
      '<VirtualHost *:8080>' \
      '  ServerName localhost' \
      '  DocumentRoot /app' \
      '  MonoServerPath webforms "/usr/bin/mod-mono-server4"' \
      '  MonoApplications webforms "/:/app"' \
      '  MonoSetEnv webforms MONO_IOMAP=all' \
      '  <Location "/">' \
      '    SetHandler mono' \
      '    MonoSetServerAlias webforms' \
      '    Require all granted' \
      '  </Location>' \
      '  DirectoryIndex Default.aspx' \
      '</VirtualHost>' \
      > /etc/apache2/sites-available/000-default.conf

WORKDIR /app
COPY ./app/ /app/

EXPOSE 8080

CMD ["apache2ctl", "-D", "FOREGROUND"]

実行!

docker build -t webforms-mono .
docker run --rm -p 8080:8080 webforms-mono

う、動いた。。。
image.png

ボタンもちゃんと動く!
image.png

DBが動くか検証

ただ単純に動くだけではアプリケーションとして機能しません。次にMySQLを立ち上げて接続できるか検証します。DBの接続にはDapperを利用します

Dockerfile
FROM debian:bullseye-slim

ARG DEBIAN_FRONTEND=noninteractive

RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends --no-install-suggests \
      apache2 \
      libapache2-mod-mono \
      mono-apache-server4 \
      ca-certificates \
      wget \
      unzip \
    ; \
    echo 'Listen 8080' > /etc/apache2/ports.conf; \
    a2enmod mono || true; \
    printf 'ServerName localhost\n' > /etc/apache2/conf-available/servername.conf; \
    a2enconf servername; \
    a2dismod mpm_event || true; \
    a2enmod mpm_prefork || true; \
    printf '%s\n' \
      '<VirtualHost *:8080>' \
      '  ServerName localhost' \
      '  DocumentRoot /app' \
      '  MonoServerPath webforms "/usr/bin/mod-mono-server4"' \
      '  MonoApplications webforms "/:/app"' \
      '  MonoSetEnv webforms MONO_IOMAP=all' \
      '  <Location "/">' \
      '    SetHandler mono' \
      '    MonoSetServerAlias webforms' \
      '    Require all granted' \
      '  </Location>' \
      '  DirectoryIndex Default.aspx' \
      '</VirtualHost>' \
      > /etc/apache2/sites-available/000-default.conf

WORKDIR /app
COPY ./app/ /app/

RUN set -eux; \
    mkdir -p /tmp/nuget /app/bin; \
    # Download .nupkg files directly
    wget -O /tmp/nuget/dapper.1.42.0.nupkg https://api.nuget.org/v3-flatcontainer/dapper/1.42.0/dapper.1.42.0.nupkg; \
    wget -O /tmp/nuget/mysql.data.8.0.33.nupkg https://api.nuget.org/v3-flatcontainer/mysql.data/8.0.33/mysql.data.8.0.33.nupkg; \
    # Extract and copy the assemblies into /app/bin
    unzip -q /tmp/nuget/dapper.1.42.0.nupkg -d /tmp/nuget/dapper; \
    unzip -q /tmp/nuget/mysql.data.8.0.33.nupkg -d /tmp/nuget/mysql.data; \
    FOUND_DAPPER=; for tfm in net45 net451 net40; do \
      if [ -f "/tmp/nuget/dapper/lib/$tfm/Dapper.dll" ]; then cp "/tmp/nuget/dapper/lib/$tfm/Dapper.dll" /app/bin/; FOUND_DAPPER=1; break; fi; \
    done; \
    if [ -z "$FOUND_DAPPER" ]; then echo 'Preferred Dapper.dll not found, dumping layout:' >&2; ls -R /tmp/nuget/dapper >&2; exit 1; fi; \
    FOUND_MYSQL=; for tfm in net462 net48 net452 net45 net40; do \
      if [ -f "/tmp/nuget/mysql.data/lib/$tfm/MySql.Data.dll" ]; then cp "/tmp/nuget/mysql.data/lib/$tfm/MySql.Data.dll" /app/bin/; FOUND_MYSQL=1; break; fi; \
    done; \
    if [ -z "$FOUND_MYSQL" ]; then echo 'Preferred MySql.Data.dll not found, dumping layout:' >&2; ls -R /tmp/nuget/mysql.data >&2; exit 1; fi

EXPOSE 8080

CMD ["apache2ctl", "-D", "FOREGROUND"]
docker-compose.yml
version: "3.9"

services:
  web:
    build: .
    container_name: webforms-app
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    networks:
      - appnet

  db:
    image: mysql:8.0
    container_name: webforms-mysql
    command: ["--default-authentication-plugin=mysql_native_password"]
    environment:
      - MYSQL_ROOT_PASSWORD=secretroot
      - MYSQL_DATABASE=appdb
      - MYSQL_USER=app
      - MYSQL_PASSWORD=apppass
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-psecretroot"]
      interval: 5s
      timeout: 3s
      retries: 20
    volumes:
      - dbdata:/var/lib/mysql
    networks:
      - appnet

volumes:
  dbdata:

networks:
  appnet:
    driver: bridge

web.config
<?xml version="1.0"?>
<configuration>
  <connectionStrings>
    <add name="MySql" connectionString="Server=db;Port=3306;Database=appdb;Uid=app;Pwd=apppass;SslMode=None;" providerName="MySql.Data.MySqlClient" />
  </connectionStrings>
  <system.web>
    <customErrors mode="Off" />
    <compilation debug="true" targetFramework="4.7.2">
      <assemblies>
        <add assembly="Dapper" />
        <add assembly="MySql.Data" />
        <add assembly="System.Runtime" />
        <add assembly="System.Data" />
        <add assembly="System.Configuration" />
      </assemblies>
    </compilation>
    <pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID" />
    <globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8" culture="ja-JP" uiCulture="ja-JP"/>
  </system.web>
</configuration>

MySQLから現在時刻を取得するだけのselect now()が動くか検証します

Default.aspx.cs
using System;
using System.Configuration;
using Dapper;
using MySql.Data.MySqlClient;

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e) { }

    protected void Btn_Click(object sender, EventArgs e)
    {
        try
        {
            var cs = ConfigurationManager.ConnectionStrings["MySql"].ConnectionString;
            using (var conn = new MySqlConnection(cs))
            {
                conn.Open();
                var serverVersion = conn.ExecuteScalar<string>("select version()");
                var now = conn.ExecuteScalar<DateTime>("select now()");
                Label1.Text = $"Connected to MySQL {serverVersion}. Now: {now:u}";
            }
        }
        catch (Exception ex)
        {
            Label1.Text = "DB接続エラー: " + ex.Message + ex.StackTrace;
        }
    }
}

実行!

docker compose up -d

う、動いた。。。すごいぞ!!!アプリケーションが作れてしまうではないか。。。

image.png

ファイルIOが出来るか検証

DBが使えるということはライブラリ次第では外部連携が出来ることがわかりました。最後にファイルIOという極めて重要な操作が動くことを検証します。

※コード量が多くなってしまったのでキャプチャとコマンドラインだけ載せます

以下のようにカレントディレクトリの~/App_Data/data.txtに対して入力した文字列を書き込めるように変更しました

image.png

こんな感じで書いて追加ボタンを押下します

image.png

とりあえず書き込めたので実際のファイルを見てみます
image.png

docker exec -it webforms-app bash
cat app/App_Data/data.txt 
初期コンテンツ
まじで書き込めんのこれ?

ちゃんと書き込めてました。すごーーーー!!

ほぼ動きそう

DB、ファイルIOと基本操作が動くのでライブラリ依存にはなりますがほぼ網羅出来るかと思います。区切り文字や文字コードがWindowsと違うのでそこらへんを気をつければ動くんじゃないかな?という所感です。

思ったよりも現実味がある

遊び程度で動かしてみましたが思ったより運用できるんじゃないか?というイメージが湧きました。.NET系のレガシー技術はWindows Serverのサポート切れでリプレイスするパターンが多いのでWEBサーバーをLinuxにしてしまえばサポート切れの心配はなくなるため低コストで移植出来る可能性を十分に秘めてるんじゃないかと思います

まとめ

20年以上前でかつWindowsのみでしか動かないと言われていたフレームワークがLinuxでかつDockerで動かせるという古き良きを大事にしつつもモダン化させる日本の美学を感じさせるような感動がありました。さすがに超レガシーすぎるので「当たり前にLinuxで動く」レベルではなくやろうと思えばできる感じではありますが、そこまで予算かけたくないけど移行しなきゃいけないレベルのアプリケーションであれば選択肢の一つになるレベルではないかと思いました。昔は動かなかったけど今は動くという.NETの古いイメージを払拭できた(?)と思うので良い実験になりました。.NETは最高だ!!

https://github.com/ikuosaito1989/webforms

Discussion