web-dev-qa-db-ja.com

IDisposableオブジェクトをusingステートメントでラップしない

ユニットテストを実装できるようにコードを更新しています。これまでのところ、データベースからデータを取得するためにリポジトリを呼び出すビジネスレイヤーがあります。

ビジネスレイヤーの例:

public class ConversationLogic
{
    private IConversationData _conversationData { get; set; }

    public ConversationLogic(IConversationData conversationData)
    {
        _conversationData = conversationData;
    }

    public ConversationListModel GetConversations(int loginID)
    {
        ConversationListModel model = new ConversationListModel();

        dynamic list = _conversationData.GetConversations(loginID);

        foreach (var item in list)
        {
            ConversationModel conversation = ConvertConversation(item);

            model.Conversations.Add(conversation);
        }

        return model;
    }
}

これにより、ConversationDataのインターフェイスを渡すことができます。このConversationDataは、データベースを呼び出すリポジトリレイヤーです。

会話データリポジトリの例:

public class ConversationData : BaseMassiveTable, IConversationData
{
    public dynamic GetConversations(int loginID)
    {
        string sql = GetResourceFile("Project.SQL.GetConversations.sql", Assembly.GetExecutingAssembly());
        dynamic result = Query(sql, loginID).ToList();
        return result;
    }
}

ConversationDataは、IDisposableを実装するBaseMassiveクラスから継承します。

public class BaseMassiveTable : DynamicModel, IDisposable
{
    string connectionName;

    public BaseMassiveTable(string connectionStringName, string tableName, string primaryKeyColumn) :
        base(connectionStringName, tableName, primaryKeyColumn) 
    { 
        connectionName = connectionStringName;
    }

    public void Dispose()
    {
    }

    protected void WriteErrorToFile(int userId, string url, Exception ex)
    {
        try
        {
            string path = "~/Error/" + DateTime.Today.ToString("yyyy-MM-dd") + ".txt";
            if (!File.Exists(System.Web.HttpContext.Current.Server.MapPath(path)))
            {
                File.Create(System.Web.HttpContext.Current.Server.MapPath(path)).Close();
            }
            using (StreamWriter w = File.AppendText(System.Web.HttpContext.Current.Server.MapPath(path)))
            {
                w.WriteLine("\r\nLog Entry : ");
                w.WriteLine("{0}", DateTime.Now.ToString(CultureInfo.InvariantCulture));
                string err = "Error in: " + url +
                              ". Error Message:" + ex.Message;
                w.WriteLine(err);
                w.WriteLine(ex.StackTrace.ToString());
                w.WriteLine("__________________________");
                w.Flush();
                w.Close();
            }
        }
        catch
        {
            //What do we do here ????
        }
    }

    public string Truncate( string stringForTuncation, int maxLength)
    {
        string result = "";
        if (stringForTuncation == null)
        {
            result = null;
        }
        else if (stringForTuncation.Length <= maxLength)
        {
            result = stringForTuncation;
        }
        else
        {
            result = stringForTuncation.Substring(0, maxLength);
        }
        return result;
    }

    public static string GetResourceFile(string name, Assembly assembly)
    {
        using (Stream stream = Assembly.GetManifestResourceStream(name))
        {
            using (StreamReader reader = new StreamReader(stream))
            {
                string result = reader.ReadToEnd();
                return result;
            }
        }
    }

}

私のConversationLogicの実装でわかるように、これは現在の実装では機能しないため、IConversationDataインターフェイスをusingでラップすることはできません。

ConversationData.GetConversations()で、次のようなMassive ORM Query()が呼び出されることもわかります。

public virtual IEnumerable<dynamic> Query(string sql, params object[] args)
{
    using (var conn = OpenConnection())
    {
        var rdr = CreateCommand(sql, conn, args).ExecuteReader();
        while (rdr.Read())
        {
            yield return rdr.RecordToExpando(); ;
        }
    }
}

私が探しているのは、ConversationDataがIDisposableを実装するBaseMassiveを継承していても、リポジトリへのすべての呼び出しがその処理を管理するMassive Query()ステートメントを呼び出すため、実際にIConversationDataを使用した呼び出しをusingステートメントでラップする必要があるということです。クエリでusing()ステートメントを作成してリソースを所有しますか?

長い質問の謝罪、何か説明が必要な場合はお知らせください。

1
user1677922

callerusingを使用する必要があります。彼が実行できるようにするには、Disposeを実装する必要があります。次に、Dispose呼び出しがカスケードされ、すべてがやがて解放されます。

使い捨てリソースが注入されるクラスには、ownDisposeメソッドがあり、そこでリソースを解放する必要があります。次に、ConversationLogicクラスを使用しているユーザーも、おそらく独自のusing句を使用してそれを破棄していることを確認する必要があります。

たとえば、ConversationLogicクラスがMVCコントローラーに挿入されている場合、基本のコントローラークラス 既にDisposeメソッドを持っている という事実を利用できます。 ConversationLogicを破棄するためにオーバーライドする必要があります。これにより、リソースを大量に消費するサービスが解放されます。 .NETパイプラインは、コントローラーのDisposeメソッドを適切なタイミングで呼び出します。

class MyController : System.Web.Mvc.Controller
{
    private readonly IConversationLogic _conversationLogic;

    public MyController(IConversationLogic conversationLogic)
    { 
        _conversationLogic = conversationLogic;
    }

    public override void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_conversationLogic != null)
            {
                _conversationLogic.Dispose(true);
                _conversationLogic = null;
            }
            base.Dispose(true);
        }
    }
}

class ConversationLogic : IConversationLogic, IDisposable
{
    private readonly _conversationData;

    public ConversationLogic(IConversationData conversationData)
    {
        _conversationData = conversationData;
    }

    public override Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_conversationData != null)
            {
                _conversationData.Dispose();
                _conversationData = null;
            }
        }
        base.Dispose(disposing);
    }
}

注入されたリソースがIDisposableを実装しているかどうかわからない場合があります。確認も簡単です。

    public override Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_conversationData != null)
            {
                var d = _conversationData as IDisposable;
                if (d != null) 
                {
                    d.Dispose();
                }
                _conversationData = null;
            }
        }
        base.Dispose(disposing);
    }

このパターンが気になる場合(純粋主義者であり、リソースを作成したコードがそれを破棄していないことに気付いたため)、次の2つのオプションがあります。

  1. リソースを作成したコードはIoCコンテナーであり、IoCコンテナーが廃棄を処理するには本当に奇妙なものが必要になるため、それを乗り越えてください。

  2. 使い捨てインスタンスを注入しないでください。代わりに工場に注入します。これにより、クラスはオブジェクトの作成と破棄の両方を行い、全員を幸せにすることができます。

    class ConversationLogic: IConversationLogic, IDisposable
    {
        private readonly _conversationData;
    
        public ConversationLogic(IConversationDataFactory conversationDataFactory)
        {
            _conversationData = conversationDataFactory.GetInstance();
        }
    
        public override Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_conversationData != null)
                {
                    _conversationData.Dispose();
                    _conversationData = null;
                }
            }
            base.Dispose(disposing);
        }
    }
    

どちらの方法でも、usingを使用せず、従来のパターンに従ってDisposeオーバーライドを使用し、コントローラー(または他のオブジェクト)自体がusing句に囲まれていることを確認して、Disposeが呼び出されるようにします。

2
John Wu

現在の設計は、インターフェース分離の原則に違反しています。

使用しないメソッドに依存するように強制されるクライアントはありません。

そのため、BaseMassiveTableIDisposableインターフェースを必要としないか、ConversationDataをこの型から派生させないでください。適切な基本型ではないようです。つまり、BaseMassiveTableConversationDataの両方を派生させることができる抽象化のレベルがさらに1つ必要になる可能性がありますが、BaseMassiveTableだけがIDisposableインターフェースを追加するため、使い捨てのリソースが導入されます。

これにより、使い捨てタイプを正しく使用できるようになります。それ以外の場合は、実際に破棄する必要がないことを知るために、ConversationDataの実装を知る必要があります。この動作は一貫性がなく、他の誰かが間違いなくここで何が起こっているのか疑問に思います。彼がそれを知らない場合、実際にこれを修正しようとするかもしれませんバグそしてそれを適切に処分します。なんらかの理由で使い捨てタイプを破棄することは問題です。現在、アンマネージリソースを使用していない場合でも、将来的には使用される可能性があるため、このインターフェイスを無視すると、後でメモリリークが発生する可能性があります。

2
t3chb0t

はい。 BaseMassiveTableが何であるかを見なくても、ネイティブリソースまたは他のIDisposableリソースのいずれかを保持しているとしか想定できません。できればusingステートメントを使用して、できるだけ早くこれらのリソースを解放する必要があります。この場合、データベース接続プールのリソースを使い果たす可能性があります。

1
Jesse C. Slicer