介紹一則維護上古 ASP.NET Web Site Project的實用技巧 - 讓兩個 aspx 共享一個 aspx.cs (CodeFile) 檔案。

對企業來說,靠著 Edge IE Mode 續命,IE 的 EOS 大限可以拖到 2029,但基於別把重要的事拖到重要又緊急,早早將 IE Only 翻修到可支援 Edge/Chrome/Firefox 是明智之舉。而這形成一個特殊情境 - IE Only 網頁只需修改 HTML/CSS/JavaScript 寫法即可相容 Edge/Chrome ,伺服器端邏輯可以完全不動(當然,如果趁機優化改良甚至改用 WebAPI/MVC/Razor Page/Blazor 更好,但你知道的,If it works, don't touch it),是因應 IE EOS 修改幅度最小的做法。

在開發及測試階段,新舊版本並存可方便對照,甚至上線初期平行測試,新版零星出錯時還可請使用者改用舊版頂著,爭取一些修正時間,感覺是不錯的策略,而這可善用 ASP.NET 的 CodeFile 分離特性實現。

一般來說,Web Site 站台旳每個 .aspx 會對映專屬 .aspx.cs,例如:Form.aspx 跟 Form.aspx.cs 是成對的。但實際上,這個對映關係會由 ASPX 第一行的宣告決定,預先編譯跟即時編譯有別:

<!-- 即時編譯 -->
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Form.aspx.cs" Inherits="DEMO_Form" %>
<!-- 預先編譯 -->
<%@ page language="C#" autoeventwireup="true" inherits="DEMO_Form, Dummy.DEMO" %>

搞懂這點,我們可以靠它實現一些進階應用,例如,把原本 aspx.cs CodeFile 內容搬進獨立元件程式庫 .dll 供多個網站專案共用,或是這次提到的共享 CodeFile - 讓 Form.aspx 跟 Form-Beta.aspx 的 CodeFile 都指向 Form.aspx.cs,實現兩個 aspx 共用一份後端程式的目標。

From.aspx、Form-Beta.aspx、Form.aspx.cs 的二對一結構可順利通過 Visual Studio 編譯、偵錯,將檔案直接複製到 IIS 動態編譯執行也沒問題。但如果要用 .publishproj 預先編譯發行,便會踩到地雷 - aspnet_merge.exe 在合併 DLL 時,會為每個指定 CodeFile 的.aspx 對映自己專屬的類別物件,而 DEMO\Form.aspx、DEMO\Form-Beta.aspx 指向同一個 CodeFile (Form.aspx.cs),故二者的對映類別都叫 DEMO_Form,合併時便會爆出型別名稱重複錯誤。An error occurred when merging assemblies: ILMerge.Merge: ERROR!!: Duplicate type 'DEMO_Form' found in assembly 'App_Web_srbkivbq'.

由爬文查到的討論一面倒地主張這是 CodeFile 運作機制,無解。但我不甘心放棄美妙的共享 .cs 做法,實在不想多複製一份 Form-Beta.aspx.cs (其實也沒多複雜,但我就是覺得不夠簡潔,尤其是有數十個頁面要處理時),最後我想出一招奇技淫巧,成功克服難題。

概念是這樣的,MSBuild 機制允許我們在 csproj、publishproj 裡安插自訂 Task,排在既有的編譯、複製檔案等 Task 前後執行,之前曾用過這招解決發行專案缺檔問題。(延伸閱讀:Visual Studio Publish 網站缺檔怎麼辦? 小試 MSBuild 自訂步驟)

依照這個概念,我準備兩支 PowerShell,Clean-BetaFiles.ps1 負責複雜檔案到暫存目錄到 aspnet_merge 前將 -Beta.aspx 檔案先搬到 App_Data\BetaFiles 目錄暫存不參與編譯,Copy-BetaFiles 則在發佈檔案複製到目錄資料夾後,將 App_Data\BetaFiles 暫存檔案複製回去,但第一行的 CodeFile="Form.aspx.cs" Inherits="DEMO_Form" 需改成 inherits="DEMO_Form, Dummy.DEMO",我採用的做法是 Form-Beta.aspx 的第一行換成 Form.aspx 發佈檔的第一行。

Clean-BetaFiles.ps1 及 Copy-BetaFiles.ps1 放在 App_Data 下,因此 publishproj 最後加上兩個 Target:
(AfterTargets 要怎麼決定?如何知道 $(_PreAspnetCompileMergeSingleTargetFolder)、 $(WPPAllFilesInSingleFolder)?我的做法是由 Build and Run Log 開 Diagnostic 觀察編譯 Task 步驟,再 Log 資訊去查使用的 targets 定義檔(如:Microsoft.Web.Publishing.AspNetCompileMerge.targets、Microsoft.WebSite.Publishing.targets... 不同 VS 版本可能不同) 以及官方文件)

<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- 略 -->
  <Target Name="RemoveBetaFormAspx" AfterTargets="CopyAllFilesToSingleFolderForAspNetCompileMerge">
    <Exec Command="powershell $(_PreAspnetCompileMergeSingleTargetFolder)\App_Data\Clean-BetaFiles.ps1">
    </Exec>
  </Target>
  <Target Name="CopyBetaFormAspx" AfterTargets="CopyAllFilesToSingleFolderForPackage">
    <Exec Command="powershell $(WPPAllFilesInSingleFolder)\App_Data\Copy-BetaFiles.ps1">
    </Exec>
  </Target>

Clean-BetaFiles.ps1

$betaPath = "$PSScriptRoot\BetaFiles"
if (Test-Path $betaPath) { 	Remove-Item $betaPath -Recurse }
[System.IO.Directory]::CreateDirectory($betaPath) | Out-Null
Set-Location $PSScriptRoot\..
Get-ChildItem -Filter *-Beta.aspx -Recurse | Where-Object {	$_.FullName -inotmatch 'app_data\\' } | ForEach-Object {
	$src = $_.FullName
	$relPath = Resolve-Path -Relative $src
	$tgt = [System.IO.Path]::ChangeExtension($betaPath + $relPath, ".aspx_")
	& echo F|xcopy $src $tgt /i
	Write-Host "Move Beta File: $src"
	Remove-Item $src
}

Copy-BetaFiles.ps1

$betaPath = Join-Path $PSScriptRoot 'BetaFiles'
$publishUrl = Join-Path $PSScriptRoot '..'
Set-Location $betaPath
Get-ChildItem -Filter *.aspx_ -Recurse | ForEach-Object {
	$src = $_.FullName
	$relPath = Resolve-Path -Relative $src
	$tgt = (Join-Path $publishUrl $relPath).Replace('.aspx_', '.aspx');
	$ref = $tgt.Replace('-Beta.aspx', '.aspx')
	$rep1stLine = Get-Content $ref | Select-Object -First 1
	Get-Content $src | ForEach-Object {
		if ($rep1stLine) {
			Write-Output $rep1stLine
			$rep1stLine = $false
		}
		else { Write-Output $_ }
	} | Out-File -FilePath $tgt
	Write-Host "Copy Beta File: $tgt"
}
Set-Location $PSScriptRoot
Remove-Item $betaPath -Recurse
Remove-Item "$PSScriptRoot\*-BetaFiles.ps1"

經過一番魔改,這個違反 ASP.NET CodeFile 規則的做法就能成功編譯發佈。

發佈檔案結構如下,成功!

因為專案結構有點小複雜,我放了一份可執行範例在 Github,有需要的朋友請自取測試。(註:我測試的環境是 VS2019,.publishproj 裡 RemoveBetaFormAspx 及 CopyBetaFormAspx 的 AfterTargets 及路徑變數可能需視 VS 版本修改。)

Tips to share aspx.cs CodeFile between two .aspx and how to prevent duplicated type error while publishing.


Comments

Be the first to post a comment

Post a comment