ASP.NET Core アプリケーションから他サーバーの共有フォルダにアクセスする (ネットワーク接続プログラム版)

Page creation date :

The page you are currently viewing does not support the selected display language.

動作検証環境

Visual Studio
  • Visual Studio 2022
ASP.NET Core
  • 8 (Razor Pages, MVC)
Windows Server
  • 2022 (ASP.NET Core 動作環境)
  • 2019 (共有フォルダ配置サーバー)
IIS
  • 10.0

動作環境

すべてで検証したわけではありませんが以下の条件で概ね動作するはずです。

Visual Studio
  • ASP.NET Core プロジェクトを開発できるならなんでも
ASP.NET Core
  • いずれのバージョンでも可 (MVC, Razor Pages, API 問わず)
Windows Server
  • Windows Server 2008 以降
IIS
  • 7.0 以降

前提条件

  • ASP.NET Core アプリケーションは IIS 上で動かすこと前提としています。
  • Windows の API を使用して認証を行うので Windows 以外では動作しません。

環境

以下のような環境で検証しています。

PC・サーバー 使用目的
Windows 11 (ローカル) プログラムを開発するための環境。
SV2022Test IIS と ASP.NET Core を動かす環境。ここから SV2019Test の共有フォルダにアクセス
SV2019Test 共有フォルダをもつサーバー

また、各種設定は以下のようにします。

パラメータ名
アクセスユーザー名 SharedUser
共有フォルダ名 SharedFolder

共有フォルダサーバーの構築

ユーザーの作成

共通フォルダにアクセスするためのユーザーを作成します。 今回ローカルアカウントを作成しますが Active Directory などのドメインでサーバーやアカウントを扱っている場合はそちらでも構いません。

ユーザーの作成手順は本 Tips の本質ではありませんので特に詳しくは説明しません。

今回は SharedUser という名前で作成します。 このユーザーで画面を操作したり設定を変えることはないのでパスワードは変更できないようにしています。

デフォルトのままだとこのユーザーでリモートデスクトップなどでログイン出来てしまうのでグループから Users を削除してください。

共有フォルダの作成

作成する場所はどこでも構いません。他サーバーからすれば物理フォルダの場所は意識しないからです。 今回は C ドライブの直下に SharedFolder という名前でフォルダを作成してそこを共有します。

プロパティを開いて共有設定を行います。

共有フォルダ名は SharedFolder にします。この名前が他サーバーから見える名前になります。 アクセス許可で SharedUser を追加してください。

既存の Everyone は削除します。

「変更」権限をつけて確定します。

あくまでも外からアクセスできる権限を付けただけなので、内部的に SharedUser がこのフォルダ内を操作できるように設定します。

「変更」権限をつけて確定します。

動作確認用にファイルを作成しておきます。 今回プログラムではエンコードを UFT-8 で処理をするので UTF-8 で保存してください。

他の PC からエクスプローラーで \\<サーバー名>\ にアクセスし、SharedUser でログインしてファイルが見れれば OK です。

ASP.NET Core アプリケーションから共有フォルダのファイル読み込み・書き込みをするプログラムを作る

サンプルの動作としてはボタンをクリックしたら、

  • 共有フォルダのファイルを読み込んで画面に表示する
  • 共有フォルダに新しいファイルを書き込む

の処理を行います。

例として Razor Pages と MVC のコードを載せていますが、共有フォルダにアクセスするプログラム自体はどちらも同じです。Web API も同様です。 なんならクライアントプログラムでも同様に動作するはずです。

特定の処理の間ネットワーク接続する処理

プロジェクトの任意の場所に以下のコードを作成します。クラス名は SharedFolderAccessor としていますが名前は任意です。 SharedFolderAccessor のインスタンスを作成し Dispose するまでの間共有フォルダにアクセスすることができます。 using を使えば明示的なスコープでアクセスできる期間を指定することが可能です。

using System.ComponentModel;
using System.Net;
using System.Runtime.InteropServices;

/// <summary>
/// 共有フォルダにユーザー名とパスワードでアクセスするためのクラスです。
/// using を使用すればそのスコープの間、共有フォルダにアクセスできます。
/// </summary>
public class SharedFolderAccessor : IDisposable
{
  private readonly string _networkName;

  /// <summary>
  /// コンストラクタです。
  /// </summary>
  /// <param name="networkName">共有フォルダのあるサーバーを「\\&lt;サーバー名&gt;」形式で指定します。</param>
  /// <param name="credentials">共有フォルダにアクセスするための資格情報です。</param>
  /// <exception cref="Win32Exception"></exception>
  public SharedFolderAccessor(string networkName, NetworkCredential credentials)
  {
    _networkName = networkName;

    // 接続するネットワークの情報を設定
    var netResource = new NetResource
    {
      Scope = ResourceScope.GlobalNetwork,
      ResourceType = ResourceType.Disk,
      DisplayType = ResourceDisplaytype.Share,
      RemoteName = networkName,
    };

    // ドメインがある場合はドメイン名も指定、ない場合はユーザー名のみ
    var userName = string.IsNullOrEmpty(credentials.Domain)
        ? credentials.UserName
        : $@"{credentials.Domain}\{credentials.UserName}";

    // 共有フォルダにユーザー名とパスワードで接続
    var result = WNetAddConnection2(netResource, credentials.Password, userName, 0);

    if (result != 0)
    {
      throw new Win32Exception(result, $"共有フォルダに接続できませんでした。(エラーコード:{result})");
    }

    // 正常に接続できれば WNetCancelConnection2 を呼び出すまではプログラムで共有フォルダにアクセス可能
  }

  ~SharedFolderAccessor()
  {
    // Dispose を呼び忘れたときの保険
    WNetCancelConnection2(_networkName, 0, true);
  }

  public void Dispose()
  {
    WNetCancelConnection2(_networkName, 0, true);
    GC.SuppressFinalize(this);  // Dispose を明示的に呼んだ場合はデストラクタの処理は不要
  }

  /// <summary>
  /// ネットワーク リソースへの接続を確立し、ローカル デバイスをネットワーク リソースにリダイレクトできます。
  /// </summary>
  /// <param name="netResource">ネットワーク リソース、ローカル デバイス、ネットワーク リソース プロバイダーに関する情報など。</param>
  /// <param name="password">ネットワーク接続の作成に使用するパスワード。</param>
  /// <param name="username">接続を確立するためのユーザー名。</param>
  /// <param name="flags">接続オプションのセット。</param>
  /// <returns></returns>
  [DllImport("mpr.dll")]
  private static extern int WNetAddConnection2(NetResource netResource, string password, string username, int flags);

  /// <summary>
  /// 既存のネットワーク接続を取り消します。
  /// </summary>
  /// <param name="name">リダイレクトされたローカル デバイスまたは切断するリモート ネットワーク リソースの名前。</param>
  /// <param name="flags">接続の種類。</param>
  /// <param name="force">接続に開いているファイルまたはジョブがある場合に切断を行う必要があるかどうか。</param>
  /// <returns></returns>
  [DllImport("mpr.dll")]
  private static extern int WNetCancelConnection2(string name, int flags, bool force);

  /// <summary>
  /// NETRESOURCE 構造体を定義しています。
  /// </summary>
  [StructLayout(LayoutKind.Sequential)]
  private class NetResource
  {
    public ResourceScope Scope;
    public ResourceType ResourceType;
    public ResourceDisplaytype DisplayType;
    public int Usage;
    public string LocalName = "";
    public string RemoteName = "";
    public string Comment = "";
    public string Provider = "";
  }

  /// <summary>
  /// ネットワークリソースのスコープ。
  /// </summary>
  private enum ResourceScope : int
  {
    /// <summary>ネットワークリソースへの現在の接続。</summary>
    Connected = 1,
    /// <summary>すべてのネットワークリソース。</summary>
    GlobalNetwork = 2,
    Remembered = 3,
    Recent = 4,
    /// <summary>ユーザーの現在および既定のネットワークコンテキストに関連付けられているネットワークリソース。</summary>
    Context = 5,
  };

  /// <summary>
  /// リソースの種類。
  /// </summary>
  private enum ResourceType : int
  {
    /// <summary>印刷リソースとディスクリソースの両方のコンテナー、または印刷またはディスク以外のリソースなど。</summary>
    Any = 0,
    /// <summary>共有ディスクボリューム。</summary>
    Disk = 1,
    /// <summary>共有プリンター。</summary>
    Print = 2,
    Reserved = 8,
  }

  /// <summary>
  /// ユーザーインターフェイスで使用する必要がある表示の種類。
  /// </summary>
  private enum ResourceDisplaytype : int
  {
    /// <summary>リソースの種類を指定しないネットワークプロバイダーによって使用されます。</summary>
    Generic = 0x0,
    /// <summary>サーバーのコレクション。</summary>
    Domain = 0x01,
    /// <summary>サーバー。</summary>
    Server = 0x02,
    /// <summary>共有ポイント。</summary>
    Share = 0x03,
    File = 0x04,
    Group = 0x05,
    /// <summary>ネットワークプロバイダー。</summary>
    Network = 0x06,
    Root = 0x07,
    Shareadmin = 0x08,
    /// <summary>ディレクトリ。</summary>
    Directory = 0x09,
    Tree = 0x0a,
    Ndscontainer = 0x0b,
  }
}

Win32 API である WNetAddConnection2WNetCancelConnection2 を使用していますので Windows 環境限定での動作となります。 一応コメントはいれていますが詳しく知りたい方はネットなどで調べてみてください。 共有フォルダ以外にもネットワークリソースにアクセスするための処理を行うこともできます。

使い方は簡単で以下のように書けば using スコープの間、共有フォルダにアクセスすることが可能です。

using (new SharedFolderAccessor($@"\\{serverName}", credentials))
{
  // この間は共有フォルダにアクセスできる
}

ただ実際には WNetCancelConnection2 を呼んだタイミングで即座に接続が切れるわけではないので using スコープ以降も共有フォルダにアクセスできたりはします。

SharedFolderAccessor を使用したテストコード

共有フォルダへのアクセス処理についてはフレームワークには依存しないのでファイルを読み書きするテスト用の共通のメソッドを作成し Pazor Pages, MVC どちらでも同じように呼べるようにします。

内容は引数に渡したテキストを共有フォルダに書き込み、すでに共有フォルダにあるテキストファイルを読み込んでテキストを返す処理となっています。

using System.Net;

public static class Util
{
  public static string ReadAndWrite(string text)
  {
    var serverName = "ServerName";
    var folderName = "SharedFolder";
    var inputFileName = "Input.txt";
    var outputFileName = "Output.txt";
    var username = "SharedUser";
    var password = "password";

    var credentials = new NetworkCredential(username, password);
    using (new SharedFolderAccessor($@"\\{serverName}", credentials))
    {
      // ファイルの書き出し
      System.IO.File.WriteAllText(Path.Combine($@"\\{serverName}\{folderName}", outputFileName), text);

      // ファイルの読み込み
      return System.IO.File.ReadAllText(Path.Combine($@"\\{serverName}\{folderName}", inputFileName));
    }
  }
}

Razor Pages

Razor Pages では Index.cshtml にボタンを配置してクリックしたらテストコードを実行し、結果を画面に表示するようにしています。 共有フォルダにアクセスできない場合もあるので try-catch で囲んでいます。

Index.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SharedFolderAccessRazorPages.Pages
{
  public class IndexModel : PageModel
  {
    private readonly ILogger<IndexModel> _logger;
    public string Message { get; set; } = "";

    public IndexModel(ILogger<IndexModel> logger)
    {
      _logger = logger;
    }

    public void OnPost()
    {
      try
      {
        Message = Util.ReadAndWrite($"プログラムからの書き込み ({DateTime.Now})");
      }
      catch (Exception ex)
      {
        Message = ex.ToString();
      }
    }
  }
}

Index.cshtml

@page
@model IndexModel
@{
  ViewData["Title"] = "Home page";
}

<div class="text-center">
  <h1 class="display-4">Welcome</h1>
  <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

@* ここから追加 *@
<form method="post">
  <button type="submit">サブミット</button>
</form>

<div>
  @Model.Message
</div>
@* ここまで追加 *@

MVC

MVC の場合も同様に Index.cshtml にボタンを配置してクリックしたら共有フォルダのテストコードを呼ぶようにしています。

Controllers/HomeController.cs

// 省略

namespace SharedFolderAccessMvc.Controllers
{
  public class HomeController : Controller
  {
    // 省略

    public IActionResult Index()
    {
      return View();
    }

    // ここから追加
    [HttpPost]
    public IActionResult Index(string dummy)
    {
      try
      {
        ViewData["Message"] = Util.ReadAndWrite($"プログラムからの書き込み ({DateTime.Now})");
      }
      catch (Exception ex)
      {
        ViewData["Message"] = ex.ToString();
      }
      return View();
    }
    // ここまで追加

    public IActionResult Privacy()
    {
      return View();
    }

    // 省略
  }
}

Index.cshtml

@{
  ViewData["Title"] = "Home Page";
}

<div class="text-center">
  <h1 class="display-4">Welcome</h1>
  <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

@* ここから追加 *@
<form method="post">
  <button type="submit">サブミット</button>
</form>

<div>
  @ViewData["Message"]
</div>
@* ここまで追加 *@

動作確認

デバッグを行い正常に共有フォルダにアクセスできるか確認します。

アプリケーションサーバーの構築

プログラムの実行で共有フォルダにアクセスできていることを確認しているのでこの先の手順は不要ですが、 IIS サーバーで動作確認を行いたい場合は以下の手順で実施できます。

ここからは補足的なものなのであまり詳しくは説明せず画像メインで説明します。

IIS のインストール

サーバーマネージャーからデフォルトでインストールしてください。手順の詳細は省きます。

追加機能は不要です。

IIS の追加サービスは今回不要です。

ASP.NET Core ランタイム Hosting Bundle インストール

今回 ASP.NET Core 8 を使用しているのでそれに合わせたランタイムのインストールが必要です。以下の URL からダウンロードします。

ASP.NET Core を IIS で動かすには「Hosting Bundle」というものが必要です。 ASP.NET Core ランタイム から「Hosting Bundle」をダウンロードしてください。

ダウンロードしたらサーバー上で実行します。

ウィザードに沿ってインストールしてください。

プログラムの発行

IIS に配置するプログラムを Visual Studio からファイルとして出力します。

Windows 用に変更します。

設定が終わったら発行します。

ターゲットの場所をクリックすれば発行したファイルがあるフォルダを開けます。

全部持っていく必要はありませんがよくわからない場合はとりあえず全部持って行って構いません。 この時点ではファイルがあることだけを確認します。

Web アプリケーションの作成と配置

Windows 管理ツールから「インターネット インフォメーション サービス (IIS) マネージャー」を開きます。

サイトを作成しますが今回は最初からある「Default Web Site」をそのまま使います。

「Default Web Site」を選択した状態で「エクスプローラー」をクリックするとフォルダが開きます。 ここに発行したプログラムをコピーします。もとからあるファイルは削除して構いません。

IIS のリンクからページを開いて画面が表示されるか確認します。 Web ブラウザを先に開いて直接 URL を入力しても構いません。

動作確認

ボタンをクリックして問題なく動作することを確認します。 上記では Web サーバー内からアクセスしていますが、Web サーバーなのでほかの PC からも操作できるはずです。