- Home
- LLBLGen Pro
- Architecture
Search with WCF
Joined: 23-Jan-2005
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.
Joined: 01-Feb-2006
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
Joined: 23-Jan-2005
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.
Joined: 01-Feb-2006
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. 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