Search with WCF

Posts   
 
    
jovball
User
Posts: 441
Joined: 23-Jan-2005
# Posted on: 05-Aug-2009 15:50:41   

I am looking for suggestions on how people are implementing ad-hoc search in ASP.NET/WCF. I have read/searched through the forums. This post (http://www.llblgen.com/tinyforum/Messages.aspx?ThreadID=8174&HighLight=1) is the most relevant to my question but it is more than 2 1/2 years old. Perhaps people have some different/better ideas now.

I'll try to give an example so that my question is clear. Given a search screen for Customers, the user has a choice of searching on the following four fields: CustomerName, Country, PostalCode, and LastOrderDate. All four fields are optional and may be used in any combination.

In a two-tier situation, I could use a method like this:

EntityCollection GetCustomers(RelationPredicateBucket predicates)

(Before you ask, I am required to use a physical three-tier design due to our Enterprise Architecture standards.)

I would prefer NOT to have a method like this:

EntityCollection GetCustomers(string name, string country, string postalCode, DateTime lastOrderDate)

It feels somewhat inflexible and would require code changes for any additonal search criteria.

Any advice will be appreciated.

Walaa avatar
Walaa
Support Team
Posts: 14983
Joined: 21-Aug-2005
# Posted on: 06-Aug-2009 10:05:20   

You may pass a single object like a special formatted string of fields and their search values. Or better to pass a dictionary with Key(field name), value pairs.

simmotech
User
Posts: 1024
Joined: 01-Feb-2006
# Posted on: 06-Aug-2009 10:42:27   

I too got bored of writing custom service methods for each fetch variation just to match 'standards' so I cheated:

Here are some snippets for fetching a collection tree for anything you like from the client-side:

1) A general interface to let you do anything from the client-side 2) Server-side implementation of said-interface 3) FetchParameters which lets you specify anything you like from the client-side and the server-side code will get LLCoolJ to fetch it for you.

PS the FromQuery<T> in FetchParameters is completely untested and I don't have time to look at it further but the idea is to allow you to convert a Linq LLCoolJ query into a suitable FetchParameters instance

public interface IDirectDataService
{
    EntityCollection<T> FetchCollection<T>(string processName) where T: EntityBase2;
    EntityCollection<T> FetchCollection<T>(string processName, IPredicate filterToUse) where T: EntityBase2;
    EntityCollection<T> FetchCollection<T>(string processName, FetchParameters parameters) where T: EntityBase2;

..... Other stuff for fetching entities/typed views/typed lists etc.
}

    [BusinessService]
    public class DirectDataService: MarshalByRefObject, IDirectDataService
    {
        #region Collections
        public EntityCollection<T> FetchCollection<T>(string processName) where T: EntityBase2
        {
            return FetchCollection<T>(processName, new FetchParameters());
        }

        public EntityCollection<T> FetchCollection<T>(string processName, IPredicate filterToUse) where T: EntityBase2
        {
            return FetchCollection<T>(processName, new FetchParameters
                                                    {
                                                        Filter = filterToUse
                                                    });
        }

        public EntityCollection<T> FetchCollection<T>(string processName, FetchParameters parameters) where T: EntityBase2
        {
            Guard.NotNull(processName, "processName");
            Guard.NotNull(parameters, "parameters");

            var result = new EntityCollection<T>();

            using(var scope = new TransactionScopeWrapper("DirectDataService.FetchCollection<" + typeof(T).Name + ">: " + processName, parameters.CommandTimeOut))
            {
                scope.DataAccessAdapter.FetchEntityCollection(result, parameters.FilterBucket ?? new RelationPredicateBucket(parameters.Filter),
                                                              parameters.MaxNumberOfItemsToReturn, parameters.SortExpression,
                                                              parameters.PrefetchPath, parameters.ExcludeIncludeFieldsList,
                                                              parameters.PageNumber, parameters.PageSize);

                scope.Complete();
            }

            return result;
        }
        #endregion Collections
        ...
    }


    [Serializable]
    public class FetchParameters
    {
        static readonly MethodInfo HandleExpressionTreeMethod = typeof(LLBLGenProProviderBase).GetMethod("HandleExpressionTree", BindingFlags.Instance | BindingFlags.NonPublic);
        static readonly MethodInfo CreateRelationPredicateBucketFromSetFilterElementsMethod = typeof(LLBLGenProProvider2).GetMethod("CreateRelationPredicateBucketFromSetFilterElements", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(QueryExpression), typeof(SetAlias) }, null);

        IRelationPredicateBucket filterBucket;
        IPrefetchPath2 prefetchPath;
        ExcludeIncludeFieldsList excludeIncludeFieldsList;
        ISortExpression sortExpression;
        int maxNumberOfItemsToReturn;
        int pageNumber;
        int pageSize;
        int? commandTimeOut;
        bool allowDuplicates = true;
        IGroupByCollection groupByCollection;
        IPredicate filter;

        public static FetchParameters FromQuery<T>(IQueryable<T> query)
        {
            var toExecute = (QueryExpression) HandleExpressionTreeMethod.Invoke(query.Provider, new[] { query.Expression });
            var setAlias = ((EntityProjectionDefinition) toExecute.Projection).Alias;

            var result = new FetchParameters
                            {
                                FilterBucket = (RelationPredicateBucket) CreateRelationPredicateBucketFromSetFilterElementsMethod.Invoke(query.Provider, new object[] { toExecute, setAlias }),
                                MaxNumberOfItemsToReturn = toExecute.MaxNumberOfElementsToReturn,
                                PageNumber = toExecute.PageNumber,
                                PageSize = toExecute.PageSize,
                                SortExpression = toExecute.SorterToUse,
                                PrefetchPath = (IPrefetchPath2) toExecute.PrefetchPathToUse,
                                ExcludeIncludeFieldsList = toExecute.ExcludeIncludeFieldsListToUse
                            };

            return result;
        }

        public bool AllowDuplicates
        {
            get { return allowDuplicates; }
            set { allowDuplicates = value; }
        }

        public IGroupByCollection GroupByCollection
        {
            get { return groupByCollection; }
            set { groupByCollection = value; }
        }

        public IRelationPredicateBucket FilterBucket
        {
            get { return filterBucket; }
            set { filterBucket = value; }
        }

        public IPrefetchPath2 PrefetchPath
        {
            get { return prefetchPath; }
            set { prefetchPath = value; }
        }

        public ExcludeIncludeFieldsList ExcludeIncludeFieldsList
        {
            get { return excludeIncludeFieldsList; }
            set { excludeIncludeFieldsList = value; }
        }

        public ISortExpression SortExpression
        {
            get { return sortExpression; }
            set { sortExpression = value; }
        }

        public int MaxNumberOfItemsToReturn
        {
            get { return maxNumberOfItemsToReturn; }
            set { maxNumberOfItemsToReturn = value; }
        }

        public int PageNumber
        {
            get { return pageNumber; }
            set { pageNumber = value; }
        }

        public int PageSize
        {
            get { return pageSize; }
            set { pageSize = value; }
        }

        public int? CommandTimeOut
        {
            get { return commandTimeOut; }
            set { commandTimeOut = value; }
        }

        public IPredicate Filter
        {
            get { return FilterBucket == null ? filter : FilterBucket.PredicateExpression; }
            set
            {
                if (FilterBucket == null)
                {
                    filter = value;
                }
                else
                {
                    FilterBucket.PredicateExpression.Clear();
                    FilterBucket.PredicateExpression.Add(value);
                }
                
            }
        }

        public EntityField2 SortField
        {
            set
            {
                Guard.NotNull(value, "value");

                SortExpression = new SortExpression(value | SortOperator.Ascending);
            }
        }

        public ISortClause SortBy
        {
            set
            {
                Guard.NotNull(value, "value");

                SortExpression = new SortExpression(value);
            }
        }


    }

Cheers Simon

jovball
User
Posts: 441
Joined: 23-Jan-2005
# Posted on: 09-Aug-2009 14:28:03   

Walaa:

Can you give me a example of this? Specifically how would you convert the dictionary or delimited string values into LLBLGen predicates?

jovball
User
Posts: 441
Joined: 23-Jan-2005
# Posted on: 09-Aug-2009 14:45:24   

Simon:

I need a bit more help with this. First, can you provide an example of what the calling code might look like? Second, where are you putting these classes? Is FetchParameters also in the BusinessService project? Finally, there are a number of "unknown" types in your code example, especially in the FetchParameters class. Some I can guess at, others I have no idea.

For the first, an example is Guard.NotNull(value, "value");. I am guessing that Guard is your own helper class of some kind. I'm guessing that I might be missing some namespace references for the others. Can you tell me what namespaces I should be referencing.

Thanks for your help so far.

simmotech
User
Posts: 1024
Joined: 01-Feb-2006
# Posted on: 10-Aug-2009 06:56:45   

Hi Joel

Sorry, I just cut and pasted code previously and didn't look closely to see if it was understandable!

This is used for .NET remoting hosted on a web server but I assume that exactly the same can be done in WCF.

The concept is very simple really: The FetchParameters class is just a place to hold all of the possible bits of information needed to fetch a collection. On the client side, you just set the properties you need and leave the rest as defaults.

The Filter/SortField/SortBy are just helper properties, so the latter two just make it easier to set SortExpression and Filter can be used if all you need to specify is an IPredicate and don't want a full IRelationPredicateBucket.

You can strip out the static stuff (2 fields and a method) to make it clearer. Also, you are right that the Guard stuff just wraps "if (value == null) throw new ArgumentNullException(parameterName);" - just replace those or remove them.

FetchParameters and IDirectDataService go in any project that is common to both the client and server sides.

"string processName" is only used for logging on the server-side and so that can be removed if not required.

The two overloads that don't include FetchParameters are just for convenience and on the server side they just create a FetchParameters instance and pass it to the method that does take a FetchParameters instance so all the active code is in one method.

Client Side code might look like this:


var filter = new PredicateExpression(ShipPerformanceParameterFields.ShipID == shipId);

var prefetchPath = new PrefetchPath2(EntityType.ShipPerformanceParameterEntity)
                                        {
                                            ShipPerformanceParameterEntity.PrefetchPathShipPerformanceWarrantySteamings,
                                            ShipPerformanceParameterEntity.PrefetchPathShipPerformanceWarrantyInPorts
                                        };

var collectionFetchParameters = new FetchParameters
                                                                    {
                                                                        Filter = filter,
                                                                        PrefetchPath = prefetchPath
                                                                    };

var shipPerformanceParameters = dds.FetchCollection<ShipPerformanceParameterEntity>("Getting Ship Performance Parameters", collectionFetchParameters);

or, inlined


var collectionFetchParameters = new FetchParameters
                                                                    {
                                                                        Filter = ShipPerformanceParameterFields.ShipID == shipId,
                                                                        PrefetchPath = new PrefetchPath2(EntityType.ShipPerformanceParameterEntity)
                                                                                                        {
                                                                                                            ShipPerformanceParameterEntity.PrefetchPathShipPerformanceWarrantySteamings,
                                                                                                            ShipPerformanceParameterEntity.PrefetchPathShipPerformanceWarrantyInPorts
                                                                                                        }
                                                                    };

var shipPerformanceParameters = dds.FetchCollection<ShipPerformanceParameterEntity>("Getting Ship Performance Parameters", collectionFetchParameters);

where dds is a client-side reference to a service implementing IDirectDataService

Server side: Again the Guard stuff can be replaced with standard null check code. Ignore the BusinessService attribute - thats just the way that we publish services (we scan a set of server assembly names, find all classes marked with that attribute and publish the interfaces they implement using and publish them using

RemotingConfiguration.RegisterWellKnownServiceType(t, interfaceType.Name + ".rem", WellKnownObjectMode.SingleCall);

) Do whatever you normally do in WCF to publish the service on the interface.

I forgot all about the TransactionScopeWrapper in the server-side code. flushed You can just remove this code. Just use whatever code you normally use to get a DataAccessAdapter; optionally set the CommandTimeOut value on it (if the value passed in FetchParameters is not null); then call the FetchCollection method passing all the bits out of the FetchParameters.

Hope this makes it a bit clearer but just post if you need more info.

Cheers Simon

jovball
User
Posts: 441
Joined: 23-Jan-2005
# Posted on: 12-Aug-2009 22:18:49   

Walaa:

Any chance of you answering my previous request for an example?

Thanks for your help with this.