New Posts New Posts RSS Feed: ExecuteQueryAsync never returns
  FAQ FAQ  Forum Search   Calendar   Register Register  Login Login

ExecuteQueryAsync never returns

 Post Reply Post Reply Page  12>
Author
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Topic: ExecuteQueryAsync never returns
    Posted: 03-Jan-2010 at 5:34am
Hi,
 
I've written the code below in order to speed up the preloading of certain entities into the cache. The ExecuteQueryAsync however never call the callback routine and no exceptions are thrown. What is wrong with this code?
 
Regards,
Paul Sinnema
Diartis AG
 
  /// <summary>
  /// Pre-load all the Navigation properties
  /// </summary>
  public void LoadAllNavigationProperties()
  {
   loader = new AsynchronousLoader();
   loader.Add<CivilStatusType>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<CivilStatusType>)CivilStatusType.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Person>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Person>)Person.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<SeparationType>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<SeparationType>)SeparationType.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<CivilStatus>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<CivilStatus>)CivilStatus.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<CorrespondenceLanguage>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<CorrespondenceLanguage>)CorrespondenceLanguage.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Address>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Address>)Address.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Communication>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Communication>)Communication.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Contact>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Contact>)Contact.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Nationality>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Nationality>)Nationality.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<PostalCode>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<PostalCode>)PostalCode.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Country>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Country>)Country.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Gender>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Gender>)Gender.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<LastName>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<LastName>)LastName.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<AddressType>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<AddressType>)AddressType.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<CommunicationType>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<CommunicationType>)CommunicationType.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<ContactRelationRoleType>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<ContactRelationRoleType>)ContactRelationRoleType.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Relation>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Relation>)Relation.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<RelationType>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<RelationType>)RelationType.Where(Item => Item.IdKLIBAlias >= 0));
   loader.Add<Organization>(ModelContext.Instance.KLIBDefaultManager, (IEntityQuery<Organization>)Organization.Where(Item => Item.IdKLIBAlias >= 0));
              loader.Execute();
  }
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using IdeaBlade.EntityModel;
using System.Collections;
namespace Diartis.KLIB.KLIBLibrary
{
    /// <summary>
    /// This class take care of filling the DevForce cache asynchronously.
    /// </summary>
    public class AsynchronousLoader
    {
        private List<object> m_Items = new List<object>();
        public AsynchronousLoader()
        {
        }
        /// <summary>
        /// Add a query to the collection
        /// </summary>
        /// <param name="manager"></param>
        /// <param name="query"></param>
        public void Add<TType>(EntityManager manager, IEntityQuery<TType> query)
            where TType : Entity
        {
            AsynchronousLoaderItem<TType> item = new AsynchronousLoaderItem<TType>(manager, query);
            m_Items.Add(item);
        }
        /// <summary>
        /// Execute all added queries
        /// </summary>
        public void Execute()
        {
            foreach (var item in m_Items)
            {
                AsynchronousLoaderItemInterface itemInterface = (AsynchronousLoaderItemInterface)item;
                itemInterface.Execute();
            }
            bool allFinished = false;
            // Wait for all queries to finish
            while (!allFinished)
            {
                allFinished = true;
                foreach (var item in m_Items)
                {
                    AsynchronousLoaderItemInterface itemInterface = (AsynchronousLoaderItemInterface)item;
                    if (!itemInterface.IsFetched)
                    {
                        allFinished = false;
                        System.Threading.Thread.Sleep(10);
                    }
                }
            }
        }
        private interface AsynchronousLoaderItemInterface
        {
            void Execute();
            bool IsFetched { get; set; }
        }
        private class AsynchronousLoaderItem<TEntity> : AsynchronousLoaderItemInterface
            where TEntity : Entity
        {
            public AsynchronousLoaderItem(EntityManager manager, IEntityQuery<TEntity> query)
            {
                Manager = manager;
                Query = query;
                IsFetched = false;
            }
            private EntityManager Manager { get; set; }
            private IEntityQuery<TEntity> Query { get; set; }
            public bool IsFetched { get; set; }
            public IEnumerable Result { get; private set; }
            public void Execute()
            {
        object userStatus = new object();
        Manager.ExecuteQueryAsync(Query, EntityAsyncCompletedCallback, userStatus);
            }
            private void EntityAsyncCompletedCallback(EntityFetchedEventArgs args)
            {
                IsFetched = true;
                Result = args.Result;
            }
        }
    }
}
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 08-Jan-2010 at 9:14pm
Hi Paul -
 
That's a lot of code to wade through. I read it and nothing obvious struck me ... except that it reminds me how hard it is to do async programming in C# and how especially hard it is to chain async calls together, either one after the other (serial) or all-at-the-same-time (parallel).
 
Fortunately, you don't have to write this yourself. We have AsyncParallelTask (and the similar AsyncSerialTask) features that are meant for this job. They are documented in our Developers Guide albeit not in depth.
 
In fact, you inspired me to develop a sample which I now have running in Windows Forms, WPF, and Silverlight. I'll be posting about this shortly. Assuming you cannot wait, let me cut to the chase and show you the code that may inspire you. 
// WARNING: Does not work with Console Application!
 
using DomainModel;
using IdeaBlade.EntityModel;
 
// .. unrelated code
 
protected int Count { get; set; } 
 
public void FillCacheWithReferenceEntities() {
  
   AsyncParallelTask
     // Create with exception handler
     .Create()
     .AddExceptionHandler(x =>
       Log("!!! threw exception during reference entity loading: " +
            x.Exception.Message))
     // Build queries (++Count and LogFetch are optional)
     .AddAsyncQuery(++Count, x => Manager.Categories, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.Customers, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.Employees, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.EmployeeTerritories, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.Products, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.Regions, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.Roles, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.Suppliers, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.Territories, LogFetch)
     .AddAsyncQuery(++Count, x => Manager.UserRoles, LogFetch)
     // Execute with final callback
     .Execute(args => {
       AssertSuccess(args); // confirms that entities are in cache
       Log("Reference entities loaded.");
       WhenDone(new AsyncEventArgs<object>(null));
     });
 }
The basic motions are (1) create an AsyncParallelTask, (2) add async queries to be run in parallel, (3) Execute it with a callback, (4) have your callback do whatever it should when ALL queries have completed.
 
The callback you see here in the Execute(...) call is just an example; you can do anything you want to do here.
 
LogFetch is another arbitrary callback method. It is optional. If present, the Task will call it after that particular async query action completes.
 
The Task marshalls both the action-specific and the final callbacks to the calling thread which means that you can safely talk to the UI in these callbacks.

I assume you'll have no trouble composing an IEntityQuery<T> for each of your AddAsyncQuery calls.
 
If you can't use AddAsyncQuery, there is an AddAsyncAction alternative method that can execute any asynchronous code. It's a tiny bit trickier to write. I show how in my forthcoming sample. If you don't need it, AddAsyncQuery is preferred.
 
--
 
Please realize that your program continues execution immediately after the Task starts executing. This enables you to keep working on the UI thread, perhaps preparing screens.
 
Typically, you would keep your UI in a confined state (say with a progress bar) until the Task finished with all entities loaded.
 
I noticed you tried to block while your loader was loading. In WinForms or WPF you can add a blocking call (e.g., a While (_notFinished) {sleep(...);} loop) immediately after this Task statement; you'd release it (set _notFinished to false) in the Task callback.
 
On the other hand, what's the point? Usually we use async so that we don't block the UI.
 
In Silverlight you cannot block even if you want to do so!
 
--
 
Some readers may notice a similarity between our Task features and the Task Parallel Library (TPL) from Microsoft. We wrote ours before the TPL had left MS labs. TPL is a .NET 4 feature and we look forward to migrating to it, perhaps in DevForce 2010.
 
However, it appears TPL will not be available in Silverlight 4 (it definitely doesn't run in SL 3) per Stephen Toub in this PFX forum post: http://preview.tinyurl.com/y94n7ce . Silverlight clients desperately need this facility. We'll keep our Task library alive for them.
 
--
 
You said you are exploring async queries to "speed up the preloading." The async approach may not give you the speed-up you expect. There are thread switching costs that can dwarf the fetching costs. Async may actually take longer!
 
I've done some timings with the code you see here, running on my laptop in 2 tier with the NorthwindIB database hosted on my machine. The synchronous v. async approaches finish in about the same time. It makes no sense to go async in this environment. Of course I'm not going to run the application on my laptop :-)
 
I have no idea whether performance will improve with your database on your LAN or in n-tier deployment. You'll want to test with a variety of client PCs; their speed, memory, number of processors will matter too.
 
I'd expect you to experience improved performance with parallel async in most situations. But you better measure first.
 
--
Some alternative approaches can give even better pre-load performance.
 
An obvious choice is to cache the highly stable reference data on the client. Upon app launch, you verify that nothing has changed  and, if you're lucky, the locally cached entities are still current and you don't make any trips to the server.
 
The number of roundtrips to the server can be a big percentage of the cost here ... bigger than the cost of sending large volumes of data. If you really have a problem and you know you always run the same exact queries during the pre-load phase, you might push that work to the BOS (our middle tier). Create a server-side method that runs these queries in the BOS and returns all of the results in a single package to the client. 
 
This is an advanced techique. It makes sense ONLY if you're application is multi-tier AND you have proven network latency problems AND this parallel async tasking is not giving you what you need.
 
Don't expect me to fully explain either of these tactics in this Forum topic. I think we've got enough to read here, don't you? I bring them up only to alert you to alternatives. They could be explored at a latter date, once you've tried the AsyncParallelTask.
 
Good luck!
 


Edited by WardBell - 08-Jan-2010 at 9:22pm
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 09-Jan-2010 at 9:14am
Hi Ward,
 
Pfew... quite the read. Thanks for the explanation. Its saturday now, I'll probably be trying this on monday. I'll discuss the solution with my colleagues and see if we can implement it in our application the way you suggested (with the progressbar and all).
 
Regards,
Paul Sinnema
Diartis AG


Edited by Sinnema - 09-Jan-2010 at 9:22am
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 09-Jan-2010 at 11:54pm

Hi Ward,

Couldn't wait until monday. I've got problems with the syntax. I've implemented the code as suggested (for us this is a T4 implementation), but the compiler does not accept the query as I've input it. I have no clue what I should enter as the Query here other that what seems to be the same in your code (but clearly isn't).
 
I get the following error:
 
Error 1 The type arguments for method 'IdeaBlade.EntityModel.AsyncParallelTaskExtensions.AddAsyncQuery<T,TNext>(IdeaBlade.EntityModel.AsyncParallelTask<T>, object, System.Func<T,IdeaBlade.EntityModel.IEntityQuery<TNext>>, IdeaBlade.EntityModel.AsyncCompletedCallback<IdeaBlade.EntityModel.EntityFetchedEventArgs<TNext>>)' cannot be inferred from the usage. Try specifying the type arguments explicitly. C:\KLIB2\trunk\KLIB\KLIBDatabase\KLIBDataBase1.cs 63 4 KLIBDatabase
 
Any Ideas on what I should use for the Query parameter?
 
Regards,
Paul Sinnema
Diartis AG
 
  /// <summary>
  /// Pre-load all the Navigation properties
  /// </summary>
  public void LoadAllNavigationProperties()
  {
            AsyncParallelTask.Create()
            .AddExceptionHandler(x =>
       Log.Error("!!! threw exception during reference entity loading: " +
       x.Exception.Message))
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.AddressType, LogFetch) // Error on this line
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Contact, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.PostalCode, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Address, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.CivilStatusType, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Person, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.SeparationType, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.CivilStatus, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.CommunicationType, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Communication, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.CorrespondenceLanguage, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.ContactRelationRoleType, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Relation, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Nationality, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Country, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Organization, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.Gender, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.LastName, LogFetch)
   .AddAsyncQuery(++Count, x => ModelContext.Instance.KLIBDefaultManager.RelationType, LogFetch)
   .Execute(args =>
   {
    AssertSuccess(args); // confirms that entities are in cache
    Log("Reference entities loaded.");
    // WhenDone(new AsyncEventArgs<object>(null));
    });
  }


Edited by Sinnema - 10-Jan-2010 at 12:01am
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 10-Jan-2010 at 2:34am
Hi Ward,
 
I've found what seems to be the cause. Haven't found a solution yet. We have implemented the Entities like this:
 
  public IQueryable<AddressType> AddressType
  {
   get
   {
    return AbstractEntity.OfType<AddressType>();
   }
  }
 
We use Inheritance in our project quite extensively and we didn't feel like typing the TypeOf() syntax all the time. The extensions need an IEntityQuery type and the properties produce an IQueryable<T> type. That why the compiler chokes.
 
Regard,
Paul Sinnema
Diartis AG


Edited by Sinnema - 10-Jan-2010 at 2:36am
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 10-Jan-2010 at 3:03am
Hi Ward,
 
Me again. This seems to be the solution:
 
  public IEntityQuery<AddressType> AddressType
  {
   get
   {
    return (IEntityQuery<AddressType>)AbstractEntity.OfType<AddressType>();
   }
  }

 
I do get some runtime errors, but I think they are related to some other change we made in the EF. Will let you know if we have success. Thanks again.
 
Regards,
Paul Sinnema
Diartis AG


Edited by Sinnema - 10-Jan-2010 at 3:04am
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 10-Jan-2010 at 11:34pm
You are correct that AddAsyncQuery is an extension method that requires an IEntityQuery<T> query parameter (which IQueryable<T> is not). I mentioned this above ... and the more raw AddAsyncAction to which one could fall back if necessary. It is not necessary in your case.
 
I don't follow why you are issuing multiple queries for subclasses of AbstractEntity. Not sure what "AbstractEntity" is. But if, as I suspect, it is the base class of a TPH inheritance hierarchy AND you are bringing down all the flavors of AbstractEntity in your pre-cache logic anyway ... why don't you grab all of the AbstractEntity instances in a single trip? That is, why ask for each subtype individually when you could get all of them in one shot with a query for AbstractEntity? Just a thought.
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 15-Jan-2010 at 11:27pm
Hi Ward,
 
Well I've implemented the Async load as suggested and, as you already predicted, there was not performance boost at all. In fact the loading took longer than before. It also produced problems which seem to be related to the order in which the data is retrieved from the DB.
 
I also tried implementing the suggestion of just loading the base class (AbstractEntity) but that took even longer.
 
In an earlier attempt to speed up loading, we implemented span queries. That didn't improve the initial load but in some cases additional load (manipulating data in rows) became faster while others got slower, so we're back at the old solution again.
 
I don't think we can speed up the inial load any further at the moment. It seems we need to find a delicate balance between preloading, asynchronous and synchronous retrieval of data. Loading data into grids could be partialy synchronous (let's say the first 100 rows) and asynchronous (retrieve all in the background) and switch datasource of the grid the second the result arives. Due to time pressure in our project that has to be postponed until after our first release.
 
Thanks anyway for trying to help me out here. At least I've learned how to asynchronously add tasks to DevForce.
 
Regards,
Paul Sinnema
Diartis AG


Edited by Sinnema - 15-Jan-2010 at 11:31pm
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 19-Jan-2010 at 4:56pm
This statement worries me:
 
"It also produced problems which seem to be related to the order in which the data is retrieved from the DB"
 
Can you elaborate?
---
 
The type names in your example suggest that the entities you want to preload are pretty stable. Curious that you haven't looked into client-side offline caching. The load price is paid ONCE. Subsequently, the data are waiting on the client.
 
Not that hard to detect if one or more of these entity types have changed and need to be refreshed.
 
It's all work but not rocket science.
 
This is edge-caching in the extreme ... because the client is the edge.
 
---
 
There is ONE more thing to try: a reference entity batch retrieval service. Remote clients call it via EntityManager.InvokeServerMethod.
 
The server-side batch service builds up an EntityManager cache on the server. This it can do quickly because it is close to the database; minimal latency for the round trips. When done, it returns to the client a cache of heterogeneous entities in a single EntityCacheState object. Finally, the client imports this EntityCacheState into the client-side EntityManager.
 
This works best when you have a lot of little entity types to ship across the wire ... as one does when there are a ton of reference types like OrderStatus, Color, UsStates, UnitOfMeasure, Currency, etc.
 
You risk bogging down the middle tier if you make this too heavy. Of course you can farm it out to server-side "clients"; these are DevForce micro-applications, running on other machines near the server with 2-tier access to the database. The BOS service delegates the work to these "services" which build batches on their own time.  You pay a double remoting cost (helper service to BOS to remote client) in both directions but the server-side exchange is brisk.
 
---
 
Then you get clever. You realize that these data don't change much. So you cache them in these services ... or maybe in the BOS. This is edge-caching on the server. It can relieve pressure on the database. It doesn't require storing data on the client.
 
You'll want the reference entity batch service to go with it.
 
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 20-Jan-2010 at 12:18pm
Hi Ward,
 
Well what I believe to be the case is the following. We've implemented a mechanism that retrieves data from the cache only when it has been pre-loaded. In other cases the data comes from the DB. This mechanism (generated with T4) uses the DefaultQueryStrategy (CacheOnly or the default). When an Entity is loaded, before another Entity which is needed and should have been pre-loaded, is read this mechanism fails (for obvious reasons). The preload routine waits until all data is pre-loaded and then continues. I guess my first implementation didn't wait so causing all the strange exceptions.
 
Ok, you're the one throwing new ideas at us, tell us how we can implement them ;-)
 
We're still having trouble with the Refetch of DevForce, so we're going with a Fat Client for our first implementation. Elaborating: we've implemented a push-notification mechanism that pushes all changes of one client to all other clients. These changes are in the form of an unique entity/key combination to refetch the data on that client. The Refetch however doesn't work anymore (since v5.2.3, there is a support request for that open [waiting for anwers from us, but we fail to find the time to answer them at the moment]). As long as that mechanism isn't in place it makes no sense to deliver a Client/Server situation. I guess that this decision criples your BOS idea.
 
Im pretty interested in the Client-side cache store/reload solution. How do we efficiently detect changes in the DB/Cache?
 
Regards,
Paul
Senior Developer at Diartis AG in Switzerland.
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 21-Jan-2010 at 3:09pm
I don't know about a failure of refetch. From your remarks, I'm guessing that someone in our support team can't repro and is waiting for info from you ... which ... and I fully understand ... you haven't had time to gather and sent to us.
 
Refetch isn't that mysterious. Are you saying you can't even do a datasource-only, overwrite-changes query by PK for the entity (which is what a refetch would do)?
 
Assume a simple approach (you can complicated later). Assume a single basket of reference entity types. If any entity changes, the basket is invalidated. Assume also that you do not expect the reference basket to change much and that you will not refresh the client-side copy during a user session. Perhaps between sessions but, once loaded, that's it. Add the color "fuscia", it won't be an available color option until the next time the user launches the application.
 
Add a ReferenceEntityVersion table and entity. There is one record defined as {WellKnownGuid, DateTime, int counter}.
 
In the server, implement IEntityServerSaving. Your implementation of the required OnSaving method calls GetEntityManager on the EntityServerSavingEventArgs to get the manager with the entities that will be saved.
 
If any of those entites is an instance of a reference entity (you can just test the EntityGroups if you like), you know the basket must be invalidated.
 
You retrieve the lone ReferenceEntityVersion , update the DateTime, and increment the counter. The ReferenceEntityVersion entity is now among the entities that will be saved when you exit the OnSaving method.
 
If you're worried about concurrency, don't touch the counter; use a database table update trigger to increment it on update.
 
YOU HAVE ENSURED THAT THE SERVER KNOWS WHEN THE REFERENCE BASKET HAS CHANGED.
 
Now, on the client-side, if there is no basket on the client cache, you've got to go get it (remember that batch reference entity service I was talking about). This is probably the first time the client has been run on this PC.
 
Note that when you cached the reference entities locally on your client, you included the ReferenceEntityVersion among the reference entities in the basket. See ModelExplorer or PrismExplorer for example of how to save EntityManagerCacheState to IsolatedStorage; if you're not in Silverlight, you can save it in any wellknown location.
 
When you launch the next time, you find the local basket and load it into the EntityManager. You fetch the ReferenceEntityVersion from the server (your one trip to the server). If the two version entities match, you're good. If they don't, empty the EntityManager, delete the file on the local client, refetch the basket from the server , and save the new basket to the local file system (asynchronously of course). Continue with launch.
 
This is the foundation for something more dynamic or more granular. If you want something more dynamic, I'd would make sure that deletes are forbidden.
 
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 23-Jan-2010 at 3:41am
Hi Ward,
 
You're right. We haven't found the time (yet) to cook up a testset which reproduces the problem. We will not be able to do that until after the deadline of April 1 (no joke ;-) or sooner when our progress goes better than expected.
 
Yes, that's correct we don't get any data when doing a datasource-only, overwrite data locally refetch (the last time I tested that is about 2 months ago). Below I've posted the client-side code used to add, remove or refetch data.
 
Thanks for the description of the Local Cache Storage solution. I understand what your telling me. I also see we can use this only for data that doesn't change much (like reference tables for types (ie Gender = 'male', 'female', etc)). Data that changes regularly should not be inside this construct.
 
The single fetch from server: can we simple retrieve data on the server, transport it to the client and add it to the local cach-manager?
 
Regards,
Paul Sinnema
Diartis AG
 
 
  On the Client ...........
   /// <summary>
  /// Gets called from the server if it has something to tell
  /// </summary>
  /// <param name="userToken"></param>
  /// <param name="postalCode"></param>
  private void PushNotificationCalled(object userToken, params Object[] args)
  {
   MemoryStream stream = (MemoryStream)args[0];
   stream.Seek(0, SeekOrigin.Begin);
   BinaryFormatter bf = new BinaryFormatter();
   EntityContainer ec = (EntityContainer)bf.Deserialize(stream);
   KLIBEntityManager manager = VMContext.Instance.KLIBDefaultManager;
   Entity entity;
   EntityKey key;
   foreach (EntityInfo ei in ec.List)
   {
    if (manager.Principal != null
     && manager.Principal.Identity != null
     && ei.Principal != null
     && ei.Principal.Identity != null
     && !manager.Principal.Identity.Name.Equals(ei.Principal.Identity.Name))
    {
     switch (ei.State)
     {
      case EntityState.Added:
       key = new EntityKey(ei.Type, ei.KeyValues);
       // manager.GetBy
       manager.DefaultQueryStrategy = QueryStrategy.DataSourceOnly;
       entity = (Entity)key.ToQuery().FirstOrNullEntity();
       if (!entity.EntityAspect.IsNullEntity)
       {
        manager.AddEntity(entity);
       }
       manager.DefaultQueryStrategy = QueryStrategy.Normal;
       break;
      case EntityState.Deleted:
       key = new EntityKey(ei.Type, ei.KeyValues);
       entity = (Entity)manager.FindEntity(key, /*includeDeleted=*/true);
       if (!entity.EntityAspect.IsNullEntity)
       {
        manager.RemoveEntity(entity);
       }
       break;
      case EntityState.Modified:
       key = new EntityKey(ei.Type, ei.KeyValues);
       entity = (Entity)manager.FindEntity(key, /*includeDeleted=*/false);
       if (entity is IKLIBEntity)
       {
        IKLIBEntity klibEntity = (IKLIBEntity)entity;
        klibEntity.Refetch();
       }
       ; break;
     }
    }
   }
  }
 
   Code generated (T4) for each Entity ..........
 
    public KLIBEvent<EventArgs> Refetched = new KLIBEvent<EventArgs>();
  public void Refetch()
  {
   ModelContext.Instance.KLIBDefaultManager.RefetchEntity(this, MergeStrategy.OverwriteChanges);
   
   this.AddMessage(MessageType.Warning, User.Translator.Translate("AbstractEntity") + " " +
                   User.Translator.Translate("Refetched"), this, null);
   
   Refetched.Call(this, EventArgs.Empty);
  }
  



Edited by Sinnema - 23-Jan-2010 at 4:03am
Senior Developer at Diartis AG in Switzerland.
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 23-Jan-2010 at 6:25pm
Paul -
 
Regarding your question: "can we simple retrieve data on the server, transport it to the client and add it to the local cach-manager?"  The answer is "absolutely".
 
You are asking about the ability to invoke a server method which issues its own series of queries while on the server, gathers them into a EntityCacheState, and ships this back to the cllient as a single result. The client-side caller loads this EntityCacheState into the target EntityManager. In this way you could perform a bunch of queries on the server (no wire latency) and ship the results back in a single round trip.
 
To learn how to do this (with example), please file another support request so one of the regular support team can dig out the sample code and sent it to you.
 
Regarding the Refetch snippet: I see nothing obviously wrong with the call to manager.Refetch. I call Refetch all of the time and I know it works. Maybe there is something odd in your environment or something I'm just not seeing. Maybe you can make a small test case: spin up your manager and just try refetching entities directly. I think you'll find it works fine.
 
May I say that your T4 approach troubles me. The code you are generating here is quite generalizable. Did you consider creating a BaseEntity class and inheriting from it? The refetch method would work just fine in such a base class.
 
Personally I would avoid generating entity code if I could and would generate the bare minimum if I felt forced into it. In a version of DevForce coming to you soon, you will be able to modify the code we generate with very simple overrides; there should be know need to have your own entity code gen then ... and you'll avoid the headaches that must abound as you try to get the code generation/update timing right.
 
Regarding the PushNotification snippet: A lot going on in there. I didn't understand many of the choices. For example, you do identity checking inside the Foreach loop. At a minimum you can (and should) pull the tests on Manager principal out as they cannot change during the loop. I don't know why you're serializing/deserializing the data; we do that for you automatically.
 
Please don't toggle the managers.DefaultQueryStrategy in the middle of your process. I see this from others too. Instead, give the query itself the strategy it should have.
 
This loop is going to make a separate round trip to the server for each added and modified entity. That's going to be painful if your notification has many changes. Supposing I got 20 orders and 100 order details via push, I'd fire off two queries - an Order query and an OrderDetails query - rather than make 120 queries! Yes, you can construct a query that retrieves by multiple Ids.
 
I don't understand why you care whether the added/modified entity is in your cache given that you've marked each EntityInfo with the client user's identity (aside: a user id would have been simpler ). If I told the server I'm interested in entity X, why would I not want to receive entity X? When it's an added entity you always add it to your cache anyway.
 
Some of what I have in mind exceeds what can reasonably be said in a forum post. You might want to purchase a few hours of professional services to get you over some of these hurdles.

Cheers, W
 


Edited by WardBell - 23-Jan-2010 at 8:50pm
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 23-Jan-2010 at 6:46pm

Sorry to butt in since Ward is handling this, but re the Refetch problem -- I notice two different EntityManagers here -  VMContext.Instance.KLIBDefaultManager and ModelContext.Instance.KLIBDefaultManager.  The refetch should be occurring in the ModelContext EM, so if these are in fact two different EntityManagers you won't see the refetched data in the VMContext EM.

Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 23-Jan-2010 at 8:48pm

Oooo. Eagle eye, Kim! That could easily be the cause!

Shows the importance of trying a simple test case before you say that a framework is broken.
 
Here's another observation. I avoid having an entity perform a persistence operation on itself, such as this refetch method. If I have to do such a thing, I make sure that the entity uses its own EntityManager. Accordingly, had I written such a method, I'd have written something like:
 
    var ea = this.EntityAspect;
    // An important guard clause that was overlooked.
    if (null == ea || ea.IsDetached()) return; 
    ea.EntityManager.RefetchEntity(this, MergeStrategy.OverwriteChanges);
 
Of course this wouldn't help in this code sample where the entity's EM and the method's EM are probably different; it would merely repeat the effect Paul experiences ... in which the refetch seems not to happen (it actually does happen ... albeit in the default EntityManager).
 
Had Paul gone the way of a query instead of refetch (an EntityKeyQuery perhaps) it might have turned out differently.
 
That said, in my view we're just moving the deck chairs around. This entire method needs to be reworked ... and meticulously tested. 
 
As written there are innumerable places for surprises to hide (what happens, for example, if the entity does not implement IKLIBEntity? Answer: the entity is not fetched. Is that the answer that fulfills the intent? What happens if the IKLIBEntity-implementing entity is not in cache? Answer: Attempted refetch of a NullEntity (I think this bombs).
 
--
 
Btw, I realize that the query or refetch the way I "recommended" would not engage the additional machinery of the "Refetch" method buried in the entity, namely the lines:
 
   this.AddMessage(MessageType.Warning, User.Translator.Translate("AbstractEntity") + " " +
                   User.Translator.Translate("Refetched"), this, null);
   
   Refetched.Call(this, EventArgs.Empty);
Assuming this is desirable (and it does not seem so to me), I'd write an EntityManager extension method that I could call anywhere, e.g.:
 
   public static void KLIBRefetch(this EntityManager manager,
            EntityWrapper entity, MergeStrategy strategy) {
 
       manager.RefetchEntity(entity, strategy);
 
       // Refactor the following line please; what good is it anyway?
      //  Who cares about "AbstractEntity refetched"?
      Logger.AddMessage(MessageType.Warning,
           User.Translator.Translate("AbstractEntity") + " " +
                   User.Translator.Translate("Refetched"), this, null);
   
        Refetched.Call(entity, EventArgs.Empty);
   }
 
Presumably there is a way to find the appropriate "Logger" and "Refetched"
 
Again I come back to my conviction that such elaborate code should not be repeated in every class. It should be in one place. And again I strongly discourage the code generation described here.
 
I'm not trying to beat up on you Paul. But I can't have people reading the forum thinking that this is the IdeaBlade way. You may have your reasons and may yet get it to work. That's fine.
 


Edited by WardBell - 23-Jan-2010 at 8:53pm
Back to Top
Sinnema View Drop Down
Groupie
Groupie
Avatar

Joined: 23-Mar-2009
Location: Muri AG, EU, CH
Posts: 54
Post Options Post Options   Quote Sinnema Quote  Post ReplyReply Direct Link To This Post Posted: 23-Jan-2010 at 11:30pm

Ward and Kim,

Thanks for the (lengthy) replies. I've read all of it but I need more time to realy understand what is written here, so forgive me if I don't react to all that's written here. I do feel I need to explain a little bit more about what we had in mind when we wrote this code.
 
PLease keep in mind that we've never had any IdeaBlade seminars/courses and had to find out how things work just by reading the documentation, analyzing the samples and trial and error. We may have gotten thing backwards.
 
What we intended was to have as little concurrency as possible. We decided that the moment of concurrency should not be the moment the user saves the data but that it could be much earlier. We designed a mechanisme that would detect changes being made to the DB and send these to all clients (except of course the one issueing the change, btw that's what the test is for in the routine above). I concur with you that the refetch could be more compact (ie pack them into 1 query as much as possible) but we needed to prove our point before perfecting it.
 
The mechanism we designed works as follows:
  • A client adds or changes data on the client (we only do soft-deletes, ie set the DeletedOn date).
  • We intercept the changes in the saving and saved events on the server.
  • We then construct a package of the change and send it to all clients that are listening. (didn't know we don't have to serialize ourselves).
  • The client test whether it was it's own change and when not does the change cycle seen above.
 
This way we get that changes are noticed earlier than the moment of the save of the data on other clients. I realize the concurrency problem doesn't go away but it's detected earlier. In the version we've cooked up now the data is simply overwritten on the client and a message that this happened is added to the messageviewer (in the final version this will be more intelligent like: only add a message when the data on client was changed also and tell who changed the data).
 
What this also produces is that (when the mechanisme works as intended) we don't need to retrieve the data after a save has taken place (see code snippet below).
 
Very sharp of Kim to see the different managers but we still have only 1 manager. On each level (View, ViewModel and Model) we have a context (KLIBContext, VMContext and ModelContext). The KLIBContext and VMContext both retrieve there manager from the ModelContext, so in the end it's the same manager.
 
I do see your point about generating too much code. I think I'll move the generated code down to the AbstractEntity (the one entity inherited by all entities).
 
It's good to hear that DevForce is still making progress on how we the developers can implement it better (I like the idea of overrides etc).
 
Thanks again for helping me out. I will shurely consider buying some PS time from IdeaBlade in the near future.
 
Regards
Paul Sinnema
Diartis AG
 
        /// <summary>
        /// Save all changes to the DB
        /// </summary>
        public void SaveChanges(Guid? userToken)
        {
            if (KLIBDefaultManager.HasChanges())
            {
    SaveOptions saveOptions = new SaveOptions();
                // Exclude all entities for refetch from the DB. We can do this because of the push notification from the
                // server that updates the client's cache for any change.
                saveOptions.ExcludeFromPostSaveRefetch = EntityMetadataStore.GetSelfAndSubtypes(typeof(DomainModel.AbstractEntity));
                try
                {
                    KLIBDefaultManager.SaveChanges(saveOptions);
                }
                catch (EntityManagerSaveException ex)
                {
                    throw;
                }
            }
        }


Edited by Sinnema - 23-Jan-2010 at 11:42pm
Senior Developer at Diartis AG in Switzerland.
Back to Top
 Post Reply Post Reply Page  12>

Forum Jump Forum Permissions View Drop Down