Here is what we're going to deliver:
var keys = new List<Key>();
DisconnectedState state;
using (var session = Session.OpenDisconnected(domain)) {
// Validation is off here by default,
// so next line passes even if it violates validation rules
var p = new Person { Name = "Alex", Age = -1 }
// DisconnectedState manages its own local transactions
// and implements unit of work pattern.
try {
() => {
p.Age = 1;
throw new Exception();
}.InvokeTransactionally(); // If you don't know,
// that's the way to create and invoke anonymous
// transactional method.
// See TransactionalExceptions type.
}
catch {}
Assert.AreEqual(-1, p.Age); // Passes.
var prefetchingQuery =
Query<Customer>.All.Where(...)
.Prefetch(c => c.Orders);
// This code will run in single local and single
// database transaction. All the fetched instances
// will be cached in DisconnectedState.
for (customer c in Query<Customer>.All) {
c.Name = string.Format("{0} ({1})", // Local change
c.Name, c.Orders.Count);
keys.Add(c.Key);
}
// Nothing is changed in the database yet -
// all the changes are cached only in DisconnectedState.
// Let's detach the state
state = session.DisconnectedState;
}
// And attach it to a new Session
using (var session = Session.OpenDisconnected(domain, state)) {
// This loop won't hit the database,
// since everything it needs is already there
for (customer c in keys.Select(Query<Customer>.Single(key)) {
Console.WriteLine("Customer: {0}", c.Name);
for (order o in c.Orders)
Console.WriteLine(" Order: {0}", o.ToString());
}
// Let's accept changes now.
// They'll be persisted in a single transaction;
// version checks and validation will be performed
// for all the changed objects.
state.AcceptChanges();
}
It will be possible to extract the changes and apply them separately:
var disconnectedState = ... var session = ... // Let's create a change set containing both changes // and original versions of all the affected objects. var changeSet = disconnectedState.GetChangeSet(); // Here we clone it by serialization, // see Xtensive.Core.Helpers.Cloner var changeSetClone = Cloner.Default.Clone(changeSet); // And finally, we apply the changes to // another Session. changeSetClone.Apply(session);
Initially DisconnectedState will track:
- Entity creation, change and removal
- Similar EntitySet operations.
So when you call ApplyChanges method (btw, it's possible to pass another Session there), change set is actually applied as sequence of Entity and EntitySet method invocations on objects from this Session. In future it will be possible to replicate top-level SessionBound method calls instead of low-level changed, that must bring true action sequence replication there.
It will be possible to:
- RejectChanges. This won't affect any fetched information.
- Prevent any transparent fetches. By default they're on - i.e. such a Session works in semi-connected mode, but you can restrict them to be available in special code blocks only (I'm not fully sure about final syntax of this, my current best option is () => { ... }.InvokeConnected() ).
- Serialize DisconnectedState and deserialize it. You won't need to create Session and Domain objects on the client to make this working (in this case we will provide "default" domain resembling the structure of original Domain object; Session there will also be "closed" - it won't allow to transparently fetch anything there).
You'll see this feature working in a week or two. I'd be glad to hear any comments.
22 comments:
We discussed the subject a few days ago, but I think it is appropriate to repeat my opinion there as a comment. It’s great that DataObjects are going to support offline scenario, but there are potential problems with suggested solution. First problem is that client application requires Domain, which means it requires direct access to database; it is often unacceptable for security and other reasons. Second problem is that each Entity creating requires roundtrip to database for key generation, this might cause performance problems. And third problem I would like to mention there is that you should care about activating sessions before creating entities or executing queries. I know, you can activate ‘global’ session, but everything ‘global’ is always bad pattern for obvious reasons. I understand that these problems can’t be solved without serious refactoring of DataObjects internal structure, but may be it worth it.
"You won't need to create Session and Domain objects on the client to make this working (in this case we will provide "default" domain resembling the structure of original Domain object;" it would be great, but I do not belive we'll see it in foreseeable future.
> First problem is that client application requires Domain, which means it requires direct access to database
That's not true. It requires the Domain, but there can be a completely different one (with limited set of types). In fact, all we need there is availability of types we need.
> Second problem is that each Entity creating requires roundtrip to database for key generation, this might cause performance problems.
The same:
- This roundtrip happens once per 128 entities. Btw, the same is in many other ORM frameworks, including NH.
- Further we'll provide support for temporary keys. This isn't difficult, but since isn't really essential, we postpone this.
I think our current solution covers 80-90% of cases; moreover, people will be able to start development from this and further will simply get new temporary keys working.
> And third problem I would like to mention there is that you should care about activating sessions before creating entities or executing queries.
In other ORM you must care about passing Sessions everywhere.
> I know, you can activate ‘global’ session, but everything ‘global’ is always bad pattern for obvious reasons.
So what do you really mean saying it is global?
Ok, are threads in .NET global? What about transactions? HttpApplication instances?
> it would be great, but I do not belive we'll see it in foreseeable future.
That's actually must be pretty easy: DisconnectedState internally contains just Tuples & other system types. So technically it can be deserialized even without a Domain.
All we must do in the end is to bind it to either current or "default" Domain.
Well, I did not know there can be fake Domains, it is a surprise. By the way, creating 'fake' sessions, 'fake' transactions and 'fake' domains looks a bit hacky.
About passing sessions everywhere - you force users to use logical dependencies instead of physical it is clear mistake. If there were physical dependencies it would take 5 minutes to implement current API but not vice-versa
"Fake" is actually bad description of what I imply here. It is a client-side Domain operating with special storage provider, that throws exceptions on attempts to make anything implying database interaction. So it's a "closed" or "disconnected" domain.
> If there were physical dependencies it would take 5 minutes to implement current API but not vice-versa
No, you're wrong. Current API worth nothing without activation, and this isn't a 5-minutes problem.
Concerning "passing Sessions everywhere": ok, what about other ways of IoC? In fact, all we're doing is:
1) asking the caller to ensure appropriate Session will be on the _virtual_ stack (provided by SessionScope) when anyone requiring it will try to get it.
2) automatically activate Session of any SessionBound object when its method is invoked.
Section 1 defines particular IoC implementation agreement; section 2 extends it a bit.
"isn't a 5-minutes problem".
I'm sorry for being annoyng, but for me as for typical user it is not clear. If there were syntax like
session.Select<Person>() for queries
and
new Person(Session session) for instance creation, I would easily implement syntax
using (session.Activate())
Query<Person>.All
}
or
using (session.Activate()) {
new Person()
}
The center point here is agreement: if we'll support just your version by default, no one can rely on availability of current Session and its automatic activation by any public SessionBound method, and vice versa.
DO4 is framework for developing modular applications. Some modules with persistent types (e.g. security, sync and full-text search) can be developed by us; others can be build by anyone else. Obviously, there must be a single convenient agreement on how to pass the Session there. And since I think our own has many advantages over explicit Session passing, I have chosen it as default.
Your example with "using" is actually not typical:
- You already have a default Session in ASP.NET application;
- WPF applications run just a single UI thread, so everything is nearly the same here.
- There must be no problems with SOA applications as well: the solution there must be even simpler than what we did for ASP.NET. See e.g. Astoria Sample.
So what do you think about explicitly passing HttpContext & all the related objects everywhere? Is it a good idea?
Hi there,
when the public version contains DisconnectedState ?
It already contains it (see Storage.Disconnected); latest bits are in repository @ Google Code (see this blog, it explains how to deal with it).
But the API is subject to change until v4.1.1, + we didn't publish docs & sample for it yet. v.4.1 (this week) will include preliminary API (enough to write WPF apps), and we'll reserve up to 2 more weeks for v4.1.1 to make everything ideal there.
Thanks for clarify, waiting for final bits :-)
I get the latest version, build, install but dont have DisconnectedState, even on Session or like you write in Storage (nor Storage even). What im doing wrong?
Ok, since there are no docs... God, forgive me.
This is it, and this is some test for it.
Hopefully, this will help ;)
P.S. It was a release announcement.
It seems I must get some sleep ;)
Ok, thanks, i see it now, but in current page post you describe use like:
using (var session = Session.OpenDisconnected(domain))
but in unit test you use it like:
var state = new DisconnectedState();
using (var session = Session.Open(Domain))
{
using (state.Attach(session))
{}
}
What will be in final release?
PS: What do you mean with "It was a release announcement." ?
PS2: You have to sleep, and maybe me too (i've little child-7months, which does not sleep well :-( )
PS3: in Firefox, i cant copy/paste in this post text area :-)
> What will be in final release?
We're thinking how to simplify current API. So the current one will exist anyway, but most likely, there will be a special method like DisconnectedSession.Open hiding this actual set of calls inside it.
> PS: What do you mean with "It was a release announcement." ?
It was a joke - it seems it was the first time when upcoming functionality was described by this way (refs to code in repository).
> PS2: You have to sleep, and maybe me too (i've little child-7months, which does not sleep well :-( )
Good luck ;) I had the same "issue" ~ 1 year ago, thanks god now they are able to sleep alone ;)
> PS3: in Firefox, i cant copy/paste in this post text area :-)
Hmm, true... I can't even write the text there... The blog is hosted @ Blogger, so I'll check if this works with other blogs.
Thank you for notifying me.
Thanks for clarify :-)
PS: Still waiting until child will sleep alone, heh :-)
Post a Comment