EDIT: Version under test is DataObjects.Net 4.6.8

Hello. Please consider the following simple schema: there are 2 entities, Rack and Drawer. Each Rack can have multiple drawers. If I attach a DisconnectedState to the Session and I add a Rack with a single Drawer:

  1. Rack's "Drawers" Property shows data added in the DisconnectedState only in direct properties of EntitySet<T> or when casted to IEnumerable<Drawer>. When using the property as IQueryable<T>, it doesn't consider data added in the DisconnectedState. This seems to be a bug.
  2. Iterating added racks returns data added in the DisconnectedState only when querying the state itself. Querying the Session while the DisconnectedState is attached returns only data persisted in the DB. If there are no other problems, it's more handy that querying the Session while the DisconnectedState is attached returns data that is also in the state as more user code can be reused.

It follows a full code snippet. Look for [1] and [2] to point out the problems reported above:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xtensive.Orm;
using Xtensive.Orm.Configuration;

namespace Test
{
    class Program
    {
        private const string CONNECTION_SERVER = "localhost";
        private const string CONNECTION_PORT = "5432";
        private const string CONNECTION_DATABASE = "biobanker2";
        private const string CONNECTION_USERID = "biobanker2";
        private const string CONNECTION_PASSWORD = "biobanker2";

        static void Main(string[] args)
        {
            String stringconnection = "Server=" + CONNECTION_SERVER;
            stringconnection += ";Port=" + CONNECTION_PORT;
            stringconnection += ";Database=" + CONNECTION_DATABASE;
            stringconnection += ";User Id=" + CONNECTION_USERID;
            stringconnection += ";Password=" + CONNECTION_PASSWORD;

            DomainConfiguration configuration = new DomainConfiguration("postgresql", stringconnection)
            {
                UpgradeMode = (true ? DomainUpgradeMode.Recreate : DomainUpgradeMode.Perform)
            };

            configuration.Types.Register(typeof(Rack).Assembly);
            var domain = Domain.Build(configuration);

            using (var session = domain.OpenSession(new SessionConfiguration(SessionOptions.AutoTransactionOpenMode)))
            {
                Rack rack = null;
                Drawer drawer = null;

                rack = new Rack(session)
                {
                    Text = "First"
                };

                DisconnectedState state = new DisconnectedState();
                using (state.Attach(session))
                using (state.Connect())
                {
                    rack = new Rack(session)
                    {
                        Text = "Second"
                    };

                    drawer = new Drawer(session)
                    {
                        Text = "Drawer"
                    };

                    rack.Drawers.Add(drawer);

                    // [1] EntitySet<T> as IQueryable<T> doesn't consider data in the DisconnectState
                    Console.WriteLine(rack.Drawers.Count);                               // Prints 1
                    Console.WriteLine(rack.Drawers.Count());                             // Prints 0!!!
                    Console.WriteLine((rack.Drawers as IEnumerable<Drawer>).Count());    // Prints 1
                    foreach (var message in rack.Drawers)
                    {
                        Console.WriteLine(message.Text);
                        // Prints "Drawer" correctly
                    }

                    var query2 =
                      from message in state.All<Rack>()
                      select message;

                    foreach (var entity in query2)
                    {
                        Console.WriteLine(entity.Text);
                        // Prints Second\nFirst -> Second is in the Disconnected State
                    }

                    var query1 =
                      from message in session.Query.All<Rack>()
                      select message;

                    foreach (var entity in query1)
                    {
                        Console.WriteLine(entity.Text);
                        // [2] Prints only First: wound't be good to print also Second while DisconnectedState is attached?
                    }

                    state.ApplyChanges();
                }

                var query4  =
                    from message in session.Query.All<Rack>()
                    select message;

                foreach (var entity in query4)
                {
                    Console.WriteLine(entity.Text);
                    // Prints First\nSecond: ok, now it's persisted
                }

            }
        }
    }

    [HierarchyRoot]
    public class Rack : Entity
    {
        public Rack(Session session)
            : base(session)
        {
        }

        [Field]
        public EntitySet<Drawer> Drawers { get; set; }

        [Field, Key]
        public int Id { get; private set; }

        [Field(Length = 100)]
        public string Text { get; set; }
    }

    [HierarchyRoot]
    public class Drawer : Entity
    {
        public Drawer(Session session)
            : base(session)
        {
        }

        [Field, Key]
        public int Id { get; private set; }

        [Field(Length = 100)]
        public string Text { get; set; }
    }
}

asked Jan 14 '15 at 13:25

fpretto's gravatar image

fpretto
5223

edited Jan 14 '15 at 13:43


2 Answers:

Hello fpretto.

There is nothing wrong with it. Let me explain.

When you create a query you get data from database. Always. When you don't use DisonnectedState then DataObjects.Net (or just DO bellow) saves changes to database by itself before a query. If you attach instance of DisconnectedState to a session you tell DO that you know best when you need to save changes. And DO skip saving changes.

Disconnected state stores history of changes. In your example you sometimes create a query and sometimes get information from Disconnected state.

First case, with counts.


Console.WriteLine(rack.Drawers.Count);   // used Disconnected state.
Console.WriteLine(rack.Drawers.Count()); // query
Console.WriteLine((rack.Drawers as IEnumerable<drawer>).Count()); again Disconnected state
Why does Count() create query in the second line and use disconnected state in the third? It's very simple. EntitySet of T implements IQueriable of T at first, and than IEnumerable of T. And Count() in the second line is extension method which creates a query. And in the third line you casted EntitySet<t> to IEnumerable<t> and used method from Linq and DO enumerated elements of Disconnected state + fetched elements from database.

'foreach' statement uses enumerator and you just enumerate IEnumerable (see previous explanation).

Second case, with query to Disconnected state


var query2 =
  from message in state.All<rack>()
  select message;
foreach (var entity in query2) {
  Console.WriteLine(entity.Text);
}
Once again, you enumerate elements in disconnected state + fetched elements.

Third case.


var query1 = from message in session.Query.All<rack>() select message;
You've created a query. As I said, you deny saving changes so you receive only items which stored in database at this moment.

After that, you saved changes to database using DisconnectedState.ApplyChanges.

And finally, you left DisconnectedState using section and give back to DO rights to save changes on it's demand. So last query executes with saving changes, but there is no changes at this moment, so it is just a query.

If you have questions, please, ask in comments and I'll answer to you.

We've hardly reworked Client profile and removed DisconnectedState from DataObjects.net 5.x.

answered Jan 15 '15 at 05:06

Alexey%20Kulakov's gravatar image

Alexey Kulakov
68715

Alexey, seeing that DisconnectedState will be eventually deprecate I ask you to specify how my specific use case is solved in DO.NET 5.x. The use case is the following: I have a WinForms webapp with a lot of wizards and postbacks that work on temporary entities to render the page. During subsequent page renders, we query these WIP entities to fill page content. We don't want to commit these temporary entities in any form in the DB so we found handy storing them in DisconnectedState. Please, show us how we solve this problem in DO.NET 5.x in a separate answer so we can evaluate the migration.

(Jan 15 '15 at 11:54) fpretto fpretto's gravatar image

With regards to DisconnectedState: I'm confused, and maybe disappointed, by your answer as I have the feeling you missed potential use cases for DisconnectedState: when a DisconnectedState is connected to a session, why querying the session should just give results from DB-only instead of DB+state? DB+state seems to me the only sane default while querying DB-only should be given as an option. Instead DB-only is your default and I don't know a way to query DB+state transparently. The only way to do it is querying state.All<T>() and casting EntitySet<T> to IEnumberable when needed.

(Jan 15 '15 at 13:35) fpretto fpretto's gravatar image

...That's why I'm really eager to understand how this works in DO.NET 5.x.

(Jan 15 '15 at 13:44) fpretto fpretto's gravatar image

I added a 5.0.3 based sample with what I want to achieve. Please, answer me there.

(Jan 15 '15 at 17:27) fpretto fpretto's gravatar image

Ok, the following example is 5.0.3 based. Basically what I want to achieve is a way to transparently query the DB+state. All the prints should be 2 for the disconnected (ClientProfile) session to be of some help in my use case, IMO. Can this be achieved?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xtensive.Orm;
using Xtensive.Orm.Configuration;
using System.Diagnostics;

namespace Test
{
    class Program
    {
        private const string CONNECTION_SERVER = "localhost";
        private const string CONNECTION_PORT = "5432";
        private const string CONNECTION_DATABASE = "biobanker2";
        private const string CONNECTION_USERID = "biobanker2";
        private const string CONNECTION_PASSWORD = "biobanker2";

        static void Main(string[] args)
        {
            String stringconnection = "Server=" + CONNECTION_SERVER;
            stringconnection += ";Port=" + CONNECTION_PORT;
            stringconnection += ";Database=" + CONNECTION_DATABASE;
            stringconnection += ";User Id=" + CONNECTION_USERID;
            stringconnection += ";Password=" + CONNECTION_PASSWORD;

            DomainConfiguration configuration = new DomainConfiguration("postgresql", stringconnection)
            {
                UpgradeMode = (true ? DomainUpgradeMode.Recreate : DomainUpgradeMode.Perform)
            };

            configuration.Types.Register(typeof(Rack).Assembly);
            var domain = Domain.Build(configuration);

            using (var session = domain.OpenSession(new SessionConfiguration(SessionOptions.Default | SessionOptions.NonTransactionalReads)))
            using (var disconnected = domain.OpenSession(new SessionConfiguration(SessionOptions.ClientProfile)))
            {
                Drawer drawer1 = null;

                using (var trans = session.OpenTransaction())
                {
                    Rack rack = new Rack(session)
                    {
                        Text = "First"
                    };

                    drawer1 = new Drawer(session)
                    {
                        Text = "First"
                    };

                    trans.Complete();
                }

                using (disconnected.Activate())
                {
                    Rack rack2 = new Rack(disconnected)
                    {
                        Text = "Second"
                    };

                    Drawer drawer2 = new Drawer(disconnected)
                    {
                        Text = "Second"
                    };

                    rack2.Drawers.Add(drawer1); // This works with drawer created in
                                                // the other session and it's great!
                    rack2.Drawers.Add(drawer2);

                    var query =
                      from drawer in rack2.Drawers
                      select drawer;

                    Console.Out.WriteLine(rack2.Drawers.Count);                          // Prints 2
                    Console.Out.WriteLine(rack2.Drawers.Count());                        // Prints 0, should be 2
                    Console.Out.WriteLine(query.ToList().Count());                       // Prints 0, should be 2
                    Console.Out.WriteLine(disconnected.Query.All<Rack>().Count());       // Prints 1, should be 2
                    Console.Out.WriteLine(disconnected.Query.All<Drawer>().Count());     // Prints 1, should be 2
                }
            }
        }
    }

    [HierarchyRoot]
    public class Rack : Entity
    {
        public Rack(Session session)
            : base(session)
        {
        }

        [Field, Key]
        public int Id { get; private set; }

        [Field]
        public EntitySet<Drawer> Drawers { get; set; }

        [Field(Length = 100)]
        public string Text { get; set; }
    }

    [HierarchyRoot]
    public class Drawer : Entity
    {
        public Drawer(Session session)
            : base(session)
        {
        }

        [Field, Key]
        public int Id { get; private set; }

        [Field(Length = 100)]
        public string Text { get; set; }
    }
}

answered Jan 15 '15 at 17:25

fpretto's gravatar image

fpretto
5223

edited Feb 05 '15 at 07:45

You use right way. But unfortunately, we have a some bugs which bring you to counts equals 0.

(Jan 19 '15 at 02:51) Alexey Kulakov Alexey%20Kulakov's gravatar image

Good. What is important is agreeing on what should mean querying a ClientProfile session and if the data added to the internal state should be reported as well. Do you have a schedule on when these should be fixed? I am inclined to think the bug is serious enough and renders ClientProfile poorly usable.

(Jan 19 '15 at 12:12) fpretto fpretto's gravatar image

Of course. We'll fix it as soon as possible, I think in next version.

(Jan 20 '15 at 03:05) Alexey Kulakov Alexey%20Kulakov's gravatar image

Hello. Have you fixed this bug? DO 5.0.4 behaves exactly the same.

(Apr 29 '15 at 05:18) fpretto fpretto's gravatar image

We fixed some bugs connected with EntitySet but this behavior wasn't associated with them. We investigated reasons of EntitySet behavior and solution probably will make us to rework EntitySet Api. If solution don't take serious changes we will fix it in 5.0 branch, otherwise, it will be fixed in one of next 5.1 Beta versions.

(Apr 30 '15 at 03:36) 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