Удалите ссылки на объекты, которые живут дольше, чем располагающий экземпляр - PullRequest
0 голосов
/ 03 марта 2020

В настоящее время я разрабатываю шаблон единицы работы и репозитория поверх NHibernate (Как замечание: я не принял решение по тому или иному шаблону, поэтому, пожалуйста, не обсуждайте полезность шаблона репозитория в отношении Я ORM, который уже реализует один). Сначала я создаю в соответствии с документацией одноэлементный (с использованием конфигурации Io C) Sessionmanager, который создает экземпляры единиц работы (закрытые методы удалены для удобства чтения):

public sealed class SessionManager : ISessionManager
    {
        #region Members
        /// <summary>
        /// lock to manage thread safe access to the dictionary.
        /// </summary>
        private readonly ReaderWriterLockSlim _lock; 

        /// <summary>
        /// NHibernate sessionfactory to create sessions.
        /// </summary>
        private readonly ISessionFactory _factory;

        /// <summary>
        /// Units of Work that are already in use.
        /// </summary>
        private readonly Dictionary<string, IUnitOfWork> _units;

        /// <summary>
        /// Flag indicating if the manager got disposed.
        /// </summary>
        private bool _disposed;
        #endregion

        #region Constructors
        /// <summary>
        /// Creates a new Manager with the given Config.
        /// </summary>
        /// <param name="config"></param>
        public SessionManager(NHibernate.Cfg.Configuration config)
        {
            _lock = new ReaderWriterLockSlim();
            _units = new Dictionary<string, IUnitOfWork>();
            _factory = config
                .Configure()
                .AddAssembly(typeof(SessionManager).Assembly.FullName)
                .BuildSessionFactory();
            _disposed = false;
        }
        #endregion

        #region Methods
        /// <summary>
        /// Either creates or returns an existing unit of work.
        /// </summary>
        /// <param name="identifier">identifier of the uow</param>
        /// <param name="access">the data access that is needed for this uow.</param>
        /// <returns></returns>
        public IUnitOfWork Create(string identifier, DataAccess access)
        {
            Throw.IfNull(identifier, nameof(identifier));

            if (TryGetUnitOfWork(identifier, out var unit))
                return unit;

            return CreateOrReturn(identifier, access);
        }

        /// <summary>
        /// Disposes the given instance.
        /// </summary>
        /// <param name="unitOfWork">The unit of work that should get disposed.</param>
        public void DisposeUnitOfWork(IUnitOfWork unitOfWork)
        {
            Throw.IfNull(unitOfWork, nameof(unitOfWork));
            try
            {
                _lock.EnterWriteLock();
                if (_units.ContainsValue(unitOfWork))
                {
                    var key = _units.FirstOrDefault(x => x.Value.Equals(unitOfWork)).Key;
                    if (!string.IsNullOrWhiteSpace(key))
                        _units.Remove(key);
                }

                unitOfWork.Dispose();

            }
            finally
            {
                _lock.ExitWriteLock();
            }
            unitOfWork.Dispose();
        }

        /// <summary>
        /// Disposes managed and unmanaged ressources.
        /// </summary>
        /// <param name="disposing">Flag indicating if the call happened in the public <see cref="Dispose"/> method or the Finalizer.</param>
        void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {                   
                    foreach (var unit in _units)
                        unit.Value.Dispose();

                    _factory.Dispose();
                    _lock.Dispose();
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                _disposed = true;
            }
        }

        /// <summary>
        /// Disposes managed and unmanaged ressources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }
}

Единица работы содержит либо ISession, либо IStatelessSession для создания транзакций или необходимых репозиториев.

public sealed class UnitOfWork : IUnitOfWork
    {
        #region Members
        private bool _disposed;

        public string Identifier { get; }

        public DataAccess Access { get; }

        private readonly ISession _session;

        private readonly IStatelessSession _statelessSession;
        #endregion

        #region Constructors
        private UnitOfWork(DataAccess access, string identifier)
        {
            Access = access;
            Identifier = identifier;
            _disposed = false;
        }

        internal UnitOfWork(DataAccess access, string identifier, ISession session)
            : this(access, identifier)
        {
            _session = session;
        }

        internal UnitOfWork(DataAccess access, string identifier, IStatelessSession session)
            : this(access, identifier)
        {
            _statelessSession = session;
        }
        #endregion

        #region Methods
        public void CloseTransaction(IGenericTransaction transaction)
        {
            transaction.Dispose();
        }


        public IGenericTransaction OpenTransaction()
        {
            if (_session != null)
                return new GenericTransaction(_session.BeginTransaction());

            if (_statelessSession != null)
                return new GenericTransaction(_statelessSession.BeginTransaction());

            throw new InvalidOperationException("You tried to create a transaction without having a vaild session.");
        }

        public IGenericTransaction OpenTransaction(IsolationLevel level)
        {
            if (_session != null)
                return new GenericTransaction(_session.BeginTransaction(level), level);

            if (_statelessSession != null)
                return new GenericTransaction(_statelessSession.BeginTransaction(level), level);

            throw new InvalidOperationException("You tried to create a transaction without having a vaild session.");
        }

        public bool Equals(IUnitOfWork other)
        {
            if (other == null)
                return false;

            return Access == other.Access;
        }

        public IRepository<T> GetRepository<T>() where T : Entity<T>
        {
            switch (Access)
            {
                case DataAccess.Statefull:
                    return new StatefullRepository<T>(_session);

                case DataAccess.Stateless:
                    return new StatelessRepository<T>(_statelessSession);

                default:
                    throw new ArgumentException("Cannot determine which repository is needed.", nameof(Access));
            }
        }

        #region IDisposable Support
        void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    if (_session != null)
                        _session.Dispose();
                    if (_statelessSession != null)
                        _statelessSession.Dispose();
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                _disposed = true;
            }
        }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        // ~UnitOfWork() {
        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        //   Dispose(false);
        // }

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            // TODO: uncomment the following line if the finalizer is overridden above.
            // GC.SuppressFinalize(this);
        }
        #endregion
        #endregion
    }

Как можно видеть, репозитории создаются с использованием либо ISession, либо IStatelessSession. Оба этих интерфейса реализуют интерфейсы IDisposable, что означает, что они должны быть удалены. Поэтому в моих репозиториях также реализовано IDisposable. Здесь, однако, проблема. Теоретически я могу создать столько хранилищ, сколько захочу из одной единицы работы, например:

public void UpdatePets(List<Cat> felines, List<Dog> carnines, ISessionManager manager)
        {
            var uow = manager.Create("Pets", DataAccess.Statefull);

            using (var t = uow.OpenTransaction())
            {
                using (var catRepo = uow.GetRepository<Cat>())
                {
                    catRepo.Add(felines);
                    t.Commit();
                }
            }

            //Some Businesslogic that forbids using one Transaction to mitigate the problem of an already disposed ISession or IStatelessSession.

            using(var t = uow.OpenTransaction())
            {
                using (var dogRepo = uow.GetRepository<Dog>())
                {
                    dogRepo.Add(carnines);
                    t.Commit();
                }
            }
        }

Если это будет услуга, потребляющая мою единицу работы, и я буду использовать «стандартный» шаблон IDisposable как это:

private void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    if (_session != null)
                        _session.Dispose();
                }
                _disposed = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

Второй репозиторий генерирует исключение ObjectDisposedexception, потому что ссылка на ISession, которую я ссылаюсь во второй, происходит из моей единицы работы, и эта уже была удалена, когда мой первый репозиторий покинул блок using. Таким образом, чтобы преодолеть эту проблему, я бы сделал следующее в своем хранилище:

public void Dispose()
            {
                _session = null;
                GC.SuppressFinalize(this);
            }

Это, однако, кажется неправильным, потому что вместо правильной утилизации ссылки на объект, который я должен располагать, я просто "закрываю глаза и забываю о это "что, вероятно, никогда не является хорошим решением в программировании. Поэтому мой вопрос в основном таков: «Есть ли хороший способ правильно расположить объекты, которые могут жить дольше, чем объект, содержащий ссылку?»

1 Ответ

2 голосов
/ 03 марта 2020

Вам необходимо установить sh право собственности и использовать его, чтобы определить, какой класс отвечает за очистку каждой "вещи". Это владелец и только владелец должен вызывать Dispose, независимо от того, сколько других классов могли взаимодействовать с ним.

Владение должно быть эксклюзивным и (за исключением классов-оболочек) nested 1 - время жизни владельца должно быть не меньше, чем у принадлежащего объекта. Следуя этим двум правилам, мы можем гарантировать, что Dispose вызывается а) один раз 2 b) когда объект больше не нужен.

Репозитории не подходят для «владения» сессия. Одному сеансу может быть предоставлено несколько репозиториев (поэтому, если они являются владельцами, мы потеряли исключительную собственность), и, как намекает ваш собственный заголовок, они имеют короче время жизни, чем оно.


Некоторые общие практические правила

  • объект, переданный в качестве параметра в метод экземпляра, обычно не становится владельцем экземпляра.
  • объект, созданный экземпляр, который живет дольше, чем время жизни одного вызова метода, будет принадлежать экземпляру
  • объекта, созданного методом экземпляра, который не будет жить дольше, чем время жизни одного вызова метода будет принадлежащий методу

1 Для оболочек внутренний объект будет сначала создаваться, возможно настраиваться, а затем передаваться объекту оболочки. Таким образом, время жизни объекта-оболочки начинается после внутреннего объекта.

2 Строго не требуется. Одноразовые изделия должны выдерживать вызов Dispose несколько раз, но это больше для защиты пояса и брекетов, чем желаемой схемы.

...