はじめに
ちょっとまえにSNSでC#とLinuxについて論争がありましたが、.NET界隈からすると普通にLinuxで動くことを知っていたのでNETに携わっていない人たちとの認知の歪みはなぜだろう?と考えていました。
よくよく考えてみると.NETを知らない人たちがイメージしているのはこれだよなぁ。。。(以下の画像)
それなら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を簡単に配置でき、簡単に高度なアプリケーションを作れる素晴らしいフレームワークとなっています
そもそも動かせるのか調べてみる
さっそく「asp.net web forms linux」でググってみます。
Nginx、Linux、ASP.NET Core。。。ぐぬぬ。。。Linuxで動く気配すら感じられません。。
つぎに「asp.net web forms docker」で調べてみます。おっ、そこそこ希望が持てるかも??
↑の質問に対する回答は0件でしたorz
調べてみるとASP.NET Web FormsはWindowsコンテナを利用すれば動く!Dockerで動くのであれば完璧!と思っていたらWindowsコンテナはLinux、Macで動かない。。。振り出しに戻ったのでもう少し奮闘...
なんとか動かせるっぽい
mod_monoというライブラリを利用すると動かせるようです。こいつを使ってなんとか動かしてみます。
実はすごいMono
実はMono、ミゲル・デ・イカザというGNOMEを立ち上げた人が作っています。GNOMEとはubuntuの顔ともなっている超メジャーなデスクトップ環境です。ubuntuのデスクトップ環境は一瞬Unityになりましたがあまりにも不評すぎてGNOMEに戻った、それぐらいLinuxの顔ともなっている代表的なOSSです。そんな最強の人が作ったプロジェクトはXimian→Novell→Xamarin→Microsoftと渡り歩いてきて現在は.NETの共同開発、ソース提供など昨今の.NETにめちゃくちゃ貢献してる縁の下の力持ち的な存在となっています。Monoがなかったら今の.NETはなかったかもしれない
うごかしてみる
私はMacを利用しているのでDockerで簡単なデモが動くか検証します
アプリケーション構成
ディレクトリ構成は以下のように単一ページの構成にします
├─ Dockerfile
└─ app/
├─ Default.aspx // 表示(HTML)の定義
|--Default.aspx.cs // ロジック(C#)の定義
└─ Web.config // 設定ファイル
ボタンクリックしたら「PostBack OK」と表示されるようにします
- HTML的なやつ
<%@ 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>
- ソースコード
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");
}
}
- 設定ファイル
<?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は最低限動けばよいのでシンプルにしています
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
DBが動くか検証
ただ単純に動くだけではアプリケーションとして機能しません。次にMySQLを立ち上げて接続できるか検証します。DBの接続にはDapperを利用します
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"]
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
<?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()
が動くか検証します
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
う、動いた。。。すごいぞ!!!アプリケーションが作れてしまうではないか。。。
ファイルIOが出来るか検証
DBが使えるということはライブラリ次第では外部連携が出来ることがわかりました。最後にファイルIOという極めて重要な操作が動くことを検証します。
コード量が多くなってしまったのでキャプチャとコマンドラインだけ載せます
以下のようにカレントディレクトリの~/App_Data/data.txt
に対して入力した文字列を書き込めるように変更しました
こんな感じで書いて追加ボタンを押下します
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は最高だ!!