Hi,

The following TEST throws an NullReferenceException where we expect an exception mentionning the absence of transaction around the Query.All<t> Indeed, we didn't use SessionOptions.NonTransactionReads

---- EXCEPTION ----

Result Message: 
Test method UnitTests.Orm.DataObjects.TestExceptionWithoutOpenedTransaction threw exception: 
System.NullReferenceException: Object reference not set to an instance of an object.
Result StackTrace:  
at Xtensive.Tuples.TupleExtensions.GetFieldStateMap(Tuple target, TupleFieldState requestedState)
   at Xtensive.Orm.Providers.Persister.CreateUpdateTask(PersistAction action, Boolean validateVersion)
   at Xtensive.Orm.Providers.Persister.CreatePersistTask(PersistAction action, Boolean validateVersion)
   at Xtensive.Orm.Providers.Persister.Persist(EntityChangeRegistry registry, CommandProcessor processor)
   at Xtensive.Orm.Providers.SqlSessionHandler.Persist(EntityChangeRegistry registry, Boolean allowPartialExecution)
   at Xtensive.Orm.Session.Persist(PersistReason reason)
   at Xtensive.Orm.Session.CreateEnumerationContext()
   at Xtensive.Orm.Rse.ProviderExtensions.GetRecordSet(ExecutableProvider provider, Session session)
   at Xtensive.Orm.Linq.TranslatedQuery`1.Execute(Session session, ParameterContext parameterContext)
   at Xtensive.Orm.Linq.QueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.Count[TSource](IQueryable`1 source)
   at UnitTests.Orm.DataObjects.TestExceptionWithoutOpenedTransaction() in c:\Work-Dev\aradus\Projets\DonatelloBranches\DataObjects5\UnitTests\Orm\DataObjects.cs:line 130

---- CODE ----

    namespace UnitTests.Orm
    {
      [TestClass]
      public class DataObjects
      {
        [TestMethod, TestCategory("Transaction.Rollback")]
        public void TestExceptionWithoutOpenedTransaction()
        {
          Domain domain = TestUtils.CreateDomain(SessionOptions.ValidateEntities | SessionOptions.AutoActivation | SessionOptions.AutoSaveChanges);
          using (Session session = domain.OpenSession())
          {
            using (TransactionScope t = session.OpenTransaction())
            {
              new TestEntity();
              t.Complete();
            }
            Assert.IsTrue(Query.All<TestEntity>().Count() == 1);
          }
        }
      }

      public static class TestUtils
      {
        public static Domain CreateDomain(SessionOptions sessionOptions)
        {
          string connectionString = Configuration.Instance.DataObjectsConnectionString;

          DomainConfiguration config = new DomainConfiguration(connectionString);
          config.UpgradeMode = DomainUpgradeMode.Recreate;
          config.Types.Register(typeof(TestEntity).Assembly);

          SessionConfiguration defaultSessionConfig = config.Sessions.Default;
          if (defaultSessionConfig == null)
          {
            defaultSessionConfig = new SessionConfiguration(WellKnown.Sessions.Default);
            config.Sessions.Add(defaultSessionConfig);
          }

          defaultSessionConfig.Options = sessionOptions;
          ConnectionInfo connectionInfo = new ConnectionInfo(connectionString);
          string sqlConnectionString = new Xtensive.Sql.Drivers.SqlServer.DriverFactory().GetConnectionString(connectionInfo);
          using (System.Data.SqlClient.SqlConnection connection = new System.Data.SqlClient.SqlConnection(sqlConnectionString))
          using (DbCommand command = connection.CreateCommand())
          {
            connection.Open();
            command.CommandText = "Select * From sys.fulltext_catalogs";
            bool ftCatalogExists = false;
            using (DbDataReader reader = command.ExecuteReader())
            {
              ftCatalogExists = reader.HasRows;
            }

            if (!ftCatalogExists)
            {
              // full text catalog does not exists : creates it
              command.CommandText = "CREATE FULLTEXT CATALOG [Default] " + "WITH ACCENT_SENSITIVITY = ON " + "AS DEFAULT " + "AUTHORIZATION [dbo]";
              command.ExecuteNonQuery();

              command.CommandText = "ALTER FULLTEXT CATALOG [Default] REBUILD ";
              command.ExecuteNonQuery();
            }
          }
          return Domain.Build(config);
        }
      }

      [Serializable]
      [HierarchyRoot]
      public class TestEntity : Entity, IHasFullTextIndex
      {
        [Field, Key]
        public long Id { get; private set; }

        [Field]
        public bool IsFtEnabled { get; set; }

        [Field]
        public bool? IsFtIndexUpToDate { get; set; }

        [Field]
        public string FullText { get; set; }

        [Field]
        public bool IsCompressed { get; set; }
      }

public interface IHasFullTextIndex : IEntity
  {
    [Field]
    long Id { get; }

    [Field]
    bool IsFtEnabled { get; set; }

    [Field]
    bool? IsFtIndexUpToDate { get; set; }

    [Field(Length = Int32.MaxValue)]
    [FullText("French")]
    string FullText { get; set; }
  }

  public class EntityChangeWatcherFullText : IModule
  {
    public void OnDefinitionsBuilt(Xtensive.Orm.Building.BuildingContext context, Xtensive.Orm.Building.Definitions.DomainModelDef model)
    {

    }

    public void OnBuilt(Domain domain)
    {
      domain.SessionOpen += new EventHandler<SessionEventArgs>(domain_SessionOpen);
    }

    private void domain_SessionOpen(object sender, SessionEventArgs e)
    {
      // a session has been created
      e.Session.SystemEvents.TransactionOpened += new EventHandler<TransactionEventArgs>(Session_TransactionOpened);
      e.Session.SystemEvents.TransactionPrecommitting += new EventHandler<TransactionEventArgs>(Session_TransactionPrecommitting);
      e.Session.SystemEvents.EntityCreated += new EventHandler<EntityEventArgs>(Session_EntityCreated);
      e.Session.SystemEvents.EntityVersionInfoChanged += new EventHandler<EntityVersionInfoChangedEventArgs>(Session_EntityVersionInfoChanged);
    }

    private void Session_TransactionOpened(object sender, TransactionEventArgs e)
    {
      // transaction opened : create our list to track entity changes
      Transaction transaction = e.Transaction;
      if (transaction.IsNested)
        return;
      Session session = transaction.Session;
      if (session.IsDisconnected)
        return;

      session.Extensions.Set(new EntityChangeWatcherInfo());
    }

    private void Session_EntityCreated(object sender, EntityEventArgs e)
    {
      // entity created
      MarkEntityInSessionListOfEntitiesToIndex(e.Entity);
    }

    private void Session_EntityVersionInfoChanged(object sender, EntityVersionInfoChangedEventArgs e)
    {
      // entity has been modified, or a sub object in an entitysert has been changed
      MarkEntityInSessionListOfEntitiesToIndex(e.Entity);
    }

    private void MarkEntityInSessionListOfEntitiesToIndex(Entity entity)
    {
      // entity to list of entity to mark fot ft indexing for current session
      // (only if necessary)
      try
      {
        Session session = entity.Session;
        if (session.IsDisconnected)
          return;

        EntityChangeWatcherInfo info = session.Extensions.Get<EntityChangeWatcherInfo>();
        if (info == null)
          return;
        if (info.IgnoreThisSession)
          return;

        if (!info.EntitiesToMarkForFullTextIndexing.Contains(entity.Key))
        {
          info.EntitiesToMarkForFullTextIndexing.Add(entity.Key);
        }
      }
      catch
      {
      }
    }

    private void Session_TransactionPrecommitting(object sender, TransactionEventArgs e)
    {
      // Just before transaction commit, read the list of entities changed for session
      // and for each entity set IsFtIndexUpToDate to false, so that we can asynchronously refresh it
      Transaction transaction = e.Transaction;
      if (transaction.IsNested)
        return;
      Session session = transaction.Session;
      if (session.IsDisconnected)
        return;

      EntityChangeWatcherInfo info = session.Extensions.Get<EntityChangeWatcherInfo>();
      if (info == null)
        return;
      if (info.IgnoreThisSession)
        return;

      foreach (Key key in info.EntitiesToMarkForFullTextIndexing)
      {
        Entity entity = session.Query.SingleOrDefault(key);
        // entity may have been removed
        if (entity != null)
        {
          IHasFullTextIndex ftObject = entity as IHasFullTextIndex;
          if (ftObject != null && ftObject.IsFtIndexUpToDate != false)
          {
            ftObject.IsFtIndexUpToDate = false;
          }
        }
      }
      info.EntitiesToMarkForFullTextIndexing.Clear();
    }
  }

  class EntityChangeWatcherInfo
  {
    public bool IgnoreThisSession { get; set; }

    public HashSet<Key> EntitiesToMarkForFullTextIndexing = new HashSet<Key>();
  }
  }

asked Jun 02 '17 at 10:49

Benoit%20Nesme's gravatar image

Benoit Nesme
43202024

edited Jun 06 '17 at 02:53


One Answer:

Hello Benoit Nesme

Sorry, but the bug is not confirmed. This is mix of two other problems.

According to your example, the only thing I got is this bug reproduced which as I said we fixed in developing branch.

But the fact that Persist execution was in stack trace tells that something happened with entities between persist on transaction scope disposing and the query. I suppose you still have some handlers bound to Session.Events.TransactionCommitting event which do some changes of entities. We agreed to resolve this case too but you will have to rebind such handlers to Session.Events.TransactionPrecommitting event anyway, because we will throw an exception in such cases to prevent data loss.

answered Jun 05 '17 at 05:43

Alexey%20Kulakov's gravatar image

Alexey Kulakov
77225

I edited the post with the Session.Events for other users. I changed the TransactionCommitting event to TransactionPrecommitting and you are right I have the same result as the other ticket : http://support.x-tensive.com/question/6541/do5-no-exception-where-exception-was-expected

(Jun 07 '17 at 05:24) Benoit Nesme Benoit%20Nesme's gravatar image

Alexey, Could you explain the difference between events : TransactionCommitting and TransactionPrecommitting and when we should use one or the other ?

(Jun 08 '17 at 04:44) Benoit Nesme Benoit%20Nesme's gravatar image

Session.Events.TransactionPrecommitting is triggered when DO transaction started commit process but changes haven't persisted yet. At this point you still able to do some changes with entities and these changes will be persisted too.

Session.Events.TransactionCommitting is triggered when all changes have persisted. This event notifies you than DO is about to start committing SQL transaction (other words DbTransaction). You shouldn't change any entity here because persist operation has already finished. You can, for example, write something to log or do something not connected with entities.

(Jun 08 '17 at 05:26) Alexey Kulakov Alexey%20Kulakov's gravatar image

After Session.Events.TransactionCommitting DO performs some system operations and tries to commit DbTransaction. After this there are two events which can be triggered. If DbTransaction commit is successful when Session.Events.TransactionCommitted will rise, otherwise, Session.Events.TranactionRollbacked will be triggered. These two events can be used for logging purposes and for actions not connected with entities.

(Jun 08 '17 at 05:34) Alexey Kulakov Alexey%20Kulakov's gravatar image
Your answer
Please start posting your answer anonymously - your answer will be saved within the current session and published after you log in or create a new account. Please try to give a substantial answer, for discussions, please use comments and please do remember to vote (after you log in)!
toggle preview

powered by OSQA