QuoteReplyTopic: InvokeServerMethodAsync is calling the Server Method twice...why? Posted: 29-Sep-2009 at 1:07pm
Problem 1 -
First, just to have something to reference, these are the two async query methods on the EntityManager:
public void ExecuteQueryAsync(IEntityQuery query, AsyncCompletedCallback<EntityFetchedEventArgs> userCallback, Object userState);
public void ExecuteQueryAsync<T>(IEntityQuery<T> query, AsyncCompletedCallback<EntityFetchedEventArgs<T>> userCallback, Object userState);
So you can use either the generic or non-generic signature, and you can pass either an IEntityQuery or IEntityQuery<T>.
Part a - Casting is needed when the query has been modified by additional clauses. In this case WhereIn returns an IQueryable<T>, and I guess we can blame the CLR type system and contravariance for requiring the cast. Check out the PredicateBuilder -- it probably does the same thing as WhereIn but the Expression predicate created (an Expression<Func<T, bool>>) can be used with the .Where clause to return an IEntityQuery<T> so that casting is not required.
Part b - When querying anonymous types you'll use the non-generic overload. If you do need to cast, again because of a clause which has changed the return type, just cast to IEntityQuery.
Part c - FirstOrDefault. Unfortunately, immediate execution queries such as this don't work in async mode. That's something we'll look at addressing in a future release.
Problem 2 - The type of args.Result will depend on the signature used when calling ExecuteQueryAsync, and it will be either an IEnumerable or IEnumerable<T>. There shouldn't be any need to cast to an IQueryable or to a List<T>. If you used the non-generic signature you can use args.Result.Cast<T> if you know T.
Thank you Kimj, I followed your advice and did this in two round trips to the server, first retrieveing the customerIDs by calling InvokeServerMethodAsync and then by executing the query InvokeServerMethodAsync for the final query.
However I ran into two minor problems I want to ask you about (I'mstill a newbie, remember):
Probelm 1: From the documentation this should be possible:
var customerQ = m_mgr.EntCustomerSet .OrderBy( c => c.p_name ) .WhereIn( c => c.p_customerID , customerIDs );
Then it compiled. Is this type cast realy needed all the time? It's ugly and in general dangerous doing type casts.
And how should I type cast when calling ExecuteQueryAsync() with projected queries (where only few properties of an entity is returned by the select command)?
Even worse, how can I put such a query into ExecuteQueryAsync():
var userQ = (from user in m_mgr.EntUserSet where user.p_userID == userID select new {user, userGroups = user.p_UserGroups}).FirstOrDefault();
Whatever I try I allways get:
Error 2 Argument '1': cannot convert from 'AnonymousType#1' to 'IdeaBlade.EntityModel.IEntityQuery' C:\Visual Studio 2005\Projects\Control2NetSilver\C2NetSilver\MainPage.xaml.cs 138 30 C2NetSilver
Or something like that.
Problem2:
As in the documentation I created the GotCustomers() receiver method such:
private void GotCustomers(EntityFetchedEventArgs args) { if (args.Error != null) { WriteMessage(args.Error.Message); } else { foreach( EntCustomer c in (System.Linq.IQueryable)args.Result )
{
....
}
}
}
When running this gave me an exception: Unable to cast object of type 'System.Collections.Generic.List`1[C2NetDomainModel.EntCustomer]' to type 'System.Linq.IQueryable'.
After changing the foreach statement to:
foreach( EntCustomer c in (List<EntCustomer>)args.Result )
everything worked as expected.
I'm currently using the newest version of IdeaBlade (version 5.2.2.0). However this problem did not exist in version 5.2.1.0. What has changed?
Ah. I see. With regard to the references, sorry I wasn't helpful. One other place we had problems was at deployment. Everything worked fine on the Dev box and the build worked, but the web site on the server was missing some of the needed DLL's. I had to add a bunch of references to the Web Server project to pull in those DLL's. Once that was done, the site started working properly. Not sure if that will help either.
As for the other. We created a Service class that manages the Async stuff and handles the client-server conversation. In this sample, the Deal class can be Promoted. On the client side, the UI Command initiates the deal.Promote(user) method, which instantiates the DealPromotionService and kicks that off. Back on the server, the [AllowRpc} ends up Fetching the Deal with the passed DealId, tells that deal to promote itself, server side, and then returns back to the Service's callback, where the Service then executes an AsyncQuery to refresh the client cache, when that calls back, the Service raises a PromotionCompleted Event that the client side Deal was watching for so that it can do raise the PropertyChanged event that will update the grid/UI.
I don't know if this is a "Good" way to do this, but it is a way and dealt with some of the issues we were having with a long stack of callbacks across projects that just got too confusing and sometimes failed to ever call back.
Here's the code:
public partial class Deal
{
public void Promote(User user)
{
var dps = new DealPromotionService(user, this, PromotionCompletedEventHandler);
}
private void PromotionCompletedEventHandler(object o, DealPromotionEventArgs e)
{
...
OnDealPromoted();
}
public event EventHandler<DealPromotionEventArgs> DealPromoted;
private void OnDealPromoted()
{
...
if (DealPromoted != null)
DealPromoted(this, new DealPromotionEventArgs(this, this.Promoted));
}
}
public class DealPromotionService
{
public DealPromotionService(User user, Deal deal, EventHandler<DealPromotionEventArgs> promotionCompletedEventHandler)
{
...
PromoteAsync();
}
public event EventHandler<DealPromotionEventArgs> DealPromotionCompleted;
private void OnDealPromotionCompleted()
{
if (DealPromotionCompleted != null)
DealPromotionCompleted(this, new DealPromotionEventArgs(this.Deal, this.Deal.Promoted));
}
private void PromoteAsync()
{
var manager = LocalEntityManager.DefaultManager;
ServerMethodDelegate myDelegate = new ServerMethodDelegate(Promote);
manager.InvokeServerMethodAsync(myDelegate, PromoteAsyncCallback, this.UserState, this.User.UserId, this.Deal.DealId, this.Deal.SuppressConfirmation); //can't pass entities as parameters in 5.2.1
//calls back to PromoteAsyncCallback
}
[AllowRpc]
public static string Promote(IPrincipal principal, EntityManager em, params object[] args)
{
User user = new User((string)args[0]);
Deal deal = Deal.Fetch((long)args[1]);
return Promote(user, deal);
}
private static string Promote(User user, Deal deal)
{
//server side
deal.PromoteCore(user);
return deal.Promoted.ToString(); //return "true" or "false" but boolean does not work as a return type in [AllowRpc]
}
private void PromoteAsyncCallback(InvokeServerMethodEventArgs args)
{
if (((Guid)args.UserState).Equals(this.UserState))
{
//back on the client
bool success = Convert.ToBoolean(args.Result);
if (success)
{
//need to refresh
FetchAfterPromoteAsync();
}
else
{
OnDealPromotionCompleted();
}
}
}
private void FetchAfterPromoteAsync()
{
var qs = new QueryStrategy(FetchStrategy.DataSourceAndCache, MergeStrategy.PreserveChangesUnlessOriginalObsolete);
var qry = this.Deal.QueryRefreshInPromotion(qs);
qry.ExecuteAsync(FetchAfterPromoteCallback, this.UserState);
}
private void FetchAfterPromoteCallback(EntityFetchedEventArgs<Deal> args)
{
if (((Guid)args.UserState).Equals(this.UserState))
{
OnDealPromotionCompleted(); //finally, raise this event as the service is finished promoting
}
}
}
I apologize tj62, but it looks like a bug was introduced a few releases ago affecting the serialization of the EntityCacheState in Silverlight applications. Right now, your best option is probably to return a list of entity IDs, and then issue a query for those IDs once back on the client. This problem will be fixed in the mid-October release.
As for the SL assemblies -- the four listed above are the only DevForce assemblies you need "client" side.
First of all Skingaby and Kimj, thank you for responding.
Regarding "1" from Skingaby, you are not right as I replaced lot of code for the "..." to shorten text and leave things that are not of importance in the problematics here. This is code to evaluate the customerIDs array that is quite complex in my case needing many LINQ queries that can not be combined in one qurery. Therefore I'm doing it such to minimize client/server roundtrips. Secondly I have to learn how this function as I need to use the InvokeServerMethodAsync in other "pits" of my system.
Regarding "2" from Skingaby, I have gone through all references to check that those are set correctly at client side as well as server side, both by checking the references in the projects as well as cheking the DLLs in folders.
Yes only .SL references are at client side but none-.SL references on the server side. However there does not exist any any IdeaBlade.EntityModel.Edm.SL to set client side. Should it exist somewhere?
Those are the references at my client side:
IdeaBlade.Core.SL.dll
IdeaBlade.EntityModel.SL.dll
IdeaBlade.Linq.SL.dll
IdeaBlade.Validation.SL.dll
...all found in the Bin\Debug of the client projects.
Note that if I simplify my server method to return somethign simple like integer or string everything works fine.
Regarding the return of multiple entities -- if that's what you end up wanting to do, using the EntityCacheState is still the best approach. The call entityManager.CacheStateManager.GetCacheState() will return an ECS containing all entities in that EntityManager's cache, not just a single entity. There's also an overload to determine the list (one or more) entities to be included.
Entities can now be directly serialized, but it looks like there are still some issues in getting them returned correctly from an RPC method.
Two things:
1) I think it may be a lot easier to call query.ExecuteAsync instead of using this technique; and
2) It sounds like the Silverlight/non-Silverlight versions of the Ideablade dlls may be mixed up.
1) It looks like the ASync method is not doing any server side processing. In my situation, I had a method that called a server side proc that created records which I needed to get back to the client somehow. Even in that situation, we ended up sending an ID back from the Async method and then calling a query.ExecuteAsync in the client side callback.
C2NetDomainModelEntityManager mgr = (C2NetDomainModelEntityManager)eMgr;
...
var customerQ = mgr.EntCustomerSet
.OrderBy( c => c.p_name )
.WhereIn( c => c.p_customerID , (from d in customerIDs
select d.customerID));
customerQ.ExecuteAsync(GotCustomers, myToken);
The callback will get a collection of Customer entities in the GotCustomers args.Result property.
2) Make sure the client side Silverlight project is using the .SL versions of the Ideablade dll's and that the server side project is using the regular dll's. I've also had problems with this error when some other project in the solution has mixed up the references. We also had this in the unit tests because although the files were there, the projects in the test run folder weren't finding them. We had to make sure the right files were being copied to the unit test in its deployment configuration.
var customerQ = mgr.EntCustomerSet .OrderBy( c => c.p_name ) .WhereIn( c => c.p_customerID , (from d in customerIDs select d.customerID).ToArray() );
// return customerQ; // Actualy I need to return all EntCustomer returned from customerQ // However for test purpose I start returning the first EntCustomer firstCustomer = customerQ.FirstOrDefault(); return firstCustomer.EntityManager.CacheStateManager.GetCacheState(); }
This method works fine. But when the receiver method public void GotCustomers(InvokeServerMethodEventArgs args) receives the result there is an error in args.Error.Message saying:
Could not load file or assembly 'IdeaBlade.EntityModel.Edm, Version=5.2.2.0, Culture=neutral, PublicKeyToken=287b5094865421c0' or one of its dependencies. The system cannot find the file specified.
The assembly IdeaBlade.EntityModel.Edm is referenced by the web application and I have tried to add it also to the assembly containing the server method (C2NetDomainModel.EntCustomer.GetCustomers). But I still get the same error.
What is the reason for that?
Second question is; If it is still not possible to return Entities from the server method (Dev Force 5.2.2.0), is there any workaround to return multiple entities similar to the one I use for returning single entity in my example above?
Oh sorry, I didn't realize that Deal was an entity sub-type. Entities cannot be directly serialized, yet. You can work around this by returning an EntityCacheState holding the Deal and Contact entities. In your sample, I'm not sure in which EntityManager Deal and Contact exist, but you can do something like this as the return value from GetADealServer -
[AllowRpc] public static EntityCacheState GetADealServer(IPrincipal principal, EntityManager em, params object[] args) { var deal = GetADealCore(); Logging.LogMessage(String.Format("GetADealServer: Deal ID {0}", deal.DealId));
On the client side, you'll need to use the EntityCacheState.Merge(EntityManager, RestoreStrategy) method to merge these entities into a client-side EntityManager.
(You can remove IKnownType from the Deal definition.)
Woohoo. I added the IKnownType interface to the Deal class and it is now being returned to the client test. However, ALL of its properties, including Entity properties, for example, Deal.Contact (which is an Entity type Contact), and normal properties, for example, Deal.DateEntered (a date) or Deal.Comment (a string), are throwing a NullReferenceException.
The property inspector in the watch window shows all properties of Deal are now showing "'deal.xxxx' threw an exception of type 'System.NullReferenceException' where xxxx is the name of the property.
Do you have any hints as to how I can fix this so the Deal is coming back to the Client with all of its properties? Thanks, Simon.
Deal.GetADealServer is being called twice because of the error - the NotFound error indicates a CommunicationException, and DevForce will attempt a retry of the service call before failing.
Check the server's debuglog to see if any additional error information was logged, in addition to the InnerException and/or RemoteException properties on the EntityServerException returned to the client. The Deal type should be both serializable and a "known type". To make it a known type, implement the marker interface IKnownType or adorn with DiscoverableType(DiscoverableTypeMode.KnownType).
I have a Silverlight Unit Test project with this class:
[TestClass]
public class DealTesting : SilverlightTest
{
[TestMethod]
[Asynchronous]
public void PromoteADeal()
{
_em = new DomainModel.DomainModelEntityManager();
EnqueueCallback(() => _em.LoginAsync(null, LoginCallbackForPromoteADeal, null));
}
#region PromoteADeal
private void LoginCallbackForPromoteADeal(LoginEventArgs args2)
{
LocalEntityManager.DefaultManager = _em;
EnqueueCallback(() => Deal.GetADealAsync(args => GetADealCallback(args)));
}
public void GetADealCallback(InvokeServerMethodEventArgs args)
{
if (args.Error != null)
throw new Exception("GetADealAsync Method Failed", args.Error);
When I run this test, the client method Deal.GetADealAsync is called once, but the server method Deal.GetADealServer is called twice. Why? This has the effect of creating 2 Deals, not 1.
Also, instead of Deal.GetADealServer returning the deal in the args of the GetADealCallback, I get a server error: "The remote server returned an error: NotFound". How can I find out what the real error is? And how do I debug this?
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot delete your posts in this forum You cannot edit your posts in this forum You cannot create polls in this forum You cannot vote in polls in this forum