ASP.NET MVCの複数のWEBサイトで使用する.cssファイルをデバッグが容易な形でDLLに埋め込んで共有するやり方

ASP.NET MVCで.cssや.jsファイルを複数のWEBアプリケーションで共有するための方法です。一般的には埋め込みリソースとしてDLLにファイルを埋め込み、WEBサイトでそのファイルをダウンロードするEndpointを作成し、HTMLファイルでそのEndpointのパスを設定するという形になります。しかしこの方法だと例えば.cssファイルのデザインを変更するのに毎回DLLのコンパイルが必要でデバッグが大変です。デバッグのしやすさを確保しつつファイルを共有する方法を紹介します。

MySolution.sln
+--WebSite1.project
+--WebSite2.project
+--MyLibrary.dll -->ここにMyCss.cssファイルを格納して利用



手順は4ステップです。
1.リソースファイルとしてMyLibrary.dllにMyCss.cssファイルを埋め込む
2.MyLibrary.dllにEmbedResourceControllerを作成する
3..htmlファイルでMyCss.cssを参照する
4.デバッグしやすいようにGetStreamFuncを利用する

##1.リソースファイルとしてMyLibrary.dllにMyCss.cssファイルを埋め込む
まずはクラスライブラリプロジェクトを作成してMyCss.cssファイルを追加します。
![MyCss.png](https://qiita-image-store.s3.amazonaws.com/0/96483/1cd9f5ad-6ed2-e5b3-7948-eaf009f673f7.png)

MyCss.cssファイルを選択状態にしてF4を押すとプロパティウィンドウが開きます。ビルドアクションを埋め込まれたリソースに設定します。
![MyCss2.png](https://qiita-image-store.s3.amazonaws.com/0/96483/b2362e0c-58bb-c74b-033e-f3ab3e6243b6.png)

これでリソースとして利用可能になりました。DLLの中にバイナリとして埋め込まれC#からそのデータを読み取ることが可能です。

```csharp:GetStreamSample.cs
Stream stm = Assembly.GetExecutingAssembly().GetManifestResourceStream("リソース名");
```


リソース名は{プロジェクトの既定の名前空間}.{フォルダ名}.{ファイル名}という形になります。プロジェクトの既定の名前空間はプロジェクトのプロパティから確認可能です。
![MyCss4.png](https://qiita-image-store.s3.amazonaws.com/0/96483/b647bfb6-ae00-979f-e448-68fcd3bab055.png)

今回埋め込んだリソースの名前は{既定の名前空間}.{フォルダ名}.{ファイル名}ということでMyLibrary.Css.MyCss.cssとなります。

#2.MyLibrary.dllにEmbedResourceControllerを作成する
ASP.NET MVCでファイルをダウンロード可能にするEndpointを作成します。ファイルのダウンロードに適したActionResultはFileStreamResultクラスが良いでしょう。以下がEmbedResourceControllerクラスのソースです。このクラスをMyLibrary.dllの中に定義します。(2015/12/02クラス修正）


```csharp:EmbedResourceController.cs
using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.Web.Mvc;
using System.Web.UI;
using System.IO;

[assembly: WebResource("MyLibrary.Css.MyCss.css", "text/css")]

namespace MyLibrary
{
    public class EmbedResourceController : Controller
    {
        private static List<Assembly> AssemblyList = new List<Assembly>();

        public static Dictionary<String, String> ContentTypeMap = new Dictionary<String, String>();
        public static Func<String, Stream> GetStreamFunc = null;

        static EmbedResourceController()
        {
            InitializeContentTypeMap();
        }
        private static void InitializeContentTypeMap()
        {
            var d = ContentTypeMap;
            d[".js"] = "text/javascript";
            d[".css"] = "text/css";
            d[".jpg"] = "image/jpeg";
            d[".png"] = "image/png";
            d[".gif"] = "image/gif";
        }
        public static void AddAssembly(Assembly assembly)
        {
            AssemblyList.Add(assembly);
        }

        [Route("EmbedResource")]
        public FileStreamResult Download(String name, String contentType = null)
        {
            Stream stm = null;
            var tp = contentType ?? GetContentType(name);

            if (GetStreamFunc != null)
            {
                stm = GetStreamFunc(name);
            }
            if (stm == null)
            {
                foreach (var item in AssemblyList)
                {
                    String resourceName = item.GetManifestResourceNames().FirstOrDefault(f => f.EndsWith(name));
                    stm = item.GetManifestResourceStream(resourceName);
                }
            }
            return new FileStreamResult(stm, tp);
        }
        private string GetContentType(String name)
        {
            var ext = Path.GetExtension(name);
            var contentType = "";
            if (ContentTypeMap.TryGetValue(ext, out contentType) == true)
            {
                return contentType;
            }
            return "text/plain";
        }
    }
}
```

WEBアプリの初期化時にリソースを埋め込んだアセンブリを登録します。

```csharp:Global.asax

    public class Global : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            EmbedResourceController.AddAssembly(Assembly.GetAssembly(typeof(MyType)));
        }
    }

```

ContentTypeをカスタマイズしたいときはContentTypeMapに値を設定すればカスタマイズ可能です。リソース名は厳密に一致しなくても最初に後方一致したものが利用されるようになっています。

#3..htmlファイルでMyCss.cssを参照する
WebSite1でMyLibrary.dllを参照してルーティングを設定します。

```csharp:WebApiConfig.cs
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
    }
}
```

これで/EmbedResource?name=MyCss.cssでCSSファイルがダウンロード可能になります。ファイルの検索ロジックはざっくり後方一致で探してくれるようにしてみました。違うURLにしたい場合は自分でルーティングを設定することも可能です。

HTMLページで下記のようにパスを設定します。

```html:MyPage.html
<link href="/EmbedResource?name=MyCss.css" rel="stylesheet" />
```

これでスタイルシートが適用されるようになりました。


#4.デバッグしやすいようにGetStreamFuncを利用する
さて上記の手順によってソースが共有されることになりプログラムの質（冗長性の排除による修正コストの低下）は向上しました。ただこの方法だとデバッグのコストが高くなってしてしまっています。スタイルシートを編集する手順としてはファイルを修正→F5でブラウザを更新して見た目を確認という手順で作業をしたいと思うでしょう。しかしながら埋め込まれたリソースはその名の通りDLLの内部に埋め込まれています。ですので再度コンパイルしないとMyCss.cssファイルは更新されません。

デバッグ容易性を向上させるためにGetStreamFuncプロパティを活用します。メソッドの内部を見るとGetStreamFuncプロパティにセットしたデリゲートがDownloadメソッドの内部で呼ばれているのがわかります。このプロパティを利用するとダウンロードする対象のデータを変更することが可能です。

```csharp:EmbedResourceController.cs
        [Route("EmbedResource")]
        public FileStreamResult Download(String name, String contentType = null)
        {
            Stream stm = null;
            var tp = contentType ?? GetContentType(name);

            if (GetStreamFunc != null)
            {
                stm = GetStreamFunc(name);
            }
            if (stm == null)
            {
                foreach (var item in AssemblyList)
                {
                    String resourceName = item.GetManifestResourceNames().FirstOrDefault(f => f.EndsWith(name));
                    stm = item.GetManifestResourceStream(resourceName);
                }
            }
            return new FileStreamResult(stm, tp);
        }
```

WEBアプリケーションの初期化時にこのプロパティにデリゲートを設定します。物理フォルダパスを指定してファイルのデータを読み取る処理を作成します。
このコードが本番環境にデプロイされないようにデバッグ時だけ設定するように保険もかけておきましょう。

```csharp:Global.asax.cs
    public class Global : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
#if DEBUG
            //デバッグ時だけセット
            EmbedResourceController.GetStreamFunc = GetEmbedResource;
#endif
        }
        private Stream GetEmbedResource(String name)
        {
            if (name == "MyCss.css")
            {
                //開発環境のパスをセット
                var fileName = "C:\\Src\\MyLibrary\\Css\\MyCss.css");
                return new FileStream(fileName, FileMode.Open, FileAccess.Read);
            }
            return null;
        }
    }


```

これでMyCss.cssファイルを変更してF5を押すとスタイルの変更が反映されてスムーズなデバッグが可能になりました。今回は1ファイルだけフックするようにしてありますが、工夫次第で全ての.cssファイルをファイルから読み取る形にすることもできるでしょう。当然ながら.jsファイルやその他のファイルでも同様の手順で上記のメリットを享受することができます。


参考になりましたでしょうか？
ご利用は自由にどうぞ。