In our CRM environment users have a 1:N relation to an available hours entity which represents their actually available hours for the week. I'm looking for a way in c# to retrieve all the available hours for users that are of a specific team for the current week.
I'm new to developing in CRM and I've looked around but there seems to be a lot of ways to do this and im not sure which is best suited.
The language is C# and the version of CRM is MS CRM 4.0
I'll cover this is 3 bits: general approach, code itself and then some notes on the code (the code is commented to draw attention to certain things, though but some of them will bear further explanation outside the code).
General Approach
As you've seen, there are a couple of ways to do things but for external applications interacting with CRM via the web service it comes down to 3 main options:
Use the strongly typed methods you get from adding a web reference in web service calls to retrieve your custom entities (I think you mentioned in a previous question seeing an explosion of methods...this gets worse when you have a lot of custom entities)
Use DynamicEntity in your web service calls
FetchXML
If your system is super-simple you can normally get away with (1) but I would recommend either of (2) or (3). Using (2) means that you only really have to remember a handful of web service methods and its good if you ever move into plugins or workflow assemblies as the concepts carry across reasonably well. (3) is good if you know FetchXML and can form the appropriate query.
I normally approach these things using (2) as it's commonly found, it's a nice middle of the road approach and, like I said, your code will be reasonably easy to translate to a plugin or workflow assembly. FetchXML carries across quite well but I was never good at forming the queries - I'll cover some techniques for this later but lets go with (2).
Also, if you use DynamicEntity you shouldn't need to refresh your web references because of how you work with it and its array of Property objects (basically you get flexibility at the expense of strong typing) as you'll see in the code. If you go with (1) you get strong typing against your custom entities but you'll have to keep refreshing your WebReference depending on the cadence of changes people make to your entities.
The Code
This is in a little console application where I've added a WebReference to the CRM service and done some calls to simulate your scenario. The code should carry across to other apps like web apps. I have tried to comment it so it is probably worth a read through before moving to the next section.
(NB. I don't claim this is the world's best code, but it does seem work and should get you started)
(NB2. I made the mistake of calling my namespace for the web reference CrmService - please don't make the same mistake as me....)
static void Main(string[] args)
{
CrmService.CrmService svc = new CrmService.CrmService();
svc.CrmAuthenticationTokenValue = GetToken();
svc.UseDefaultCredentials = true;
#region 1 - Retrieve users in team
RetrieveMembersTeamRequest teamMembersReq = new RetrieveMembersTeamRequest()
{
EntityId = new Guid("D56E0E83-2198-E211-9900-080027BBBE99"), //You'll need the team GUID
ReturnDynamicEntities = true
};
ColumnSet teamMembersReqColumnSet = new ColumnSet();
teamMembersReqColumnSet.Attributes = new string[] { "systemuserid", "domainname" };
teamMembersReq.MemberColumnSet = teamMembersReqColumnSet; //Don't use: teamMembersReq.MemberColumnSet = new AllColumns()
List<Guid> userIdList = new List<Guid>();
RetrieveMembersTeamResponse teamMembersResp = svc.Execute(teamMembersReq) as RetrieveMembersTeamResponse;
if (teamMembersResp != null)
{
BusinessEntity[] usersInTeamAsBusinessEntity = teamMembersResp.BusinessEntityCollection.BusinessEntities;
List<DynamicEntity> usersInTeamAsDynEntity = usersInTeamAsBusinessEntity.Select(be => be as DynamicEntity).ToList(); //BusinessEntity not too useful, cast to DynamicEntity
foreach (DynamicEntity de in usersInTeamAsDynEntity)
{
Property userIdProp = de.Properties.Where(p => p.Name == "systemuserid").FirstOrDefault();
Property domainNameProp = de.Properties.Where(p => p.Name == "domainname").FirstOrDefault();
if (userIdProp != null)
{
KeyProperty userIdKeyProp = userIdProp as KeyProperty; //Because it is the unique identifier of the entity
userIdList.Add(userIdKeyProp.Value.Value); //Chuck in a list for use later
Console.Write("Key: " + userIdKeyProp.Value.Value.ToString());
}
if (domainNameProp != null)
{
StringProperty domainNameStringProp = domainNameProp as StringProperty; //Because its data type is varchar
Console.Write("| Domain Name: " + domainNameStringProp.Value);
}
Console.WriteLine();
}
}
#endregion
/*
* For this example I have created a dummy entity called new_availablehours that is in a 1:N relationship with use (i.e. 1 user, many new_available hours).
* The test attributes are :
* - the relationship attribute is called new_userid...this obviously links across to the GUID from systemuser
* - there is an int data type attribute called new_hours
* - there is a datetime attribute called new_availabilityday
*/
#region Retrieve From 1:N
RetrieveMultipleRequest req = new RetrieveMultipleRequest();
req.ReturnDynamicEntities = true; //Because we love DynamicEntity
//QueryExpression says what entity to retrieve from, what columns we want back and what criteria we use for selection
QueryExpression qe = new QueryExpression();
qe.EntityName = "new_availablehours"; //the entity on the many side of the 1:N which we want to get data from
qe.ColumnSet = new AllColumns(); //Don't do this in real life, limit it like we did when retrieving team members
/*
* In this case we have 1 x Filter Expression which combines multiple Condition Operators
* Condition Operators are evaluated together using the FilterExpression object's FilterOperator property (which is either AND or OR)
*
* So if we use AND all conditions need to be true and if we use OR then at least one of the conditions provided needs to be true
*
*/
FilterExpression fe = new FilterExpression();
fe.FilterOperator = LogicalOperator.And;
ConditionExpression userCondition = new ConditionExpression();
userCondition.AttributeName = "new_userid"; //The attribute of qe.EntityName which we want to test against
userCondition.Operator = ConditionOperator.In; //Because we got a list of users previously, the appropriate check is to get records where new_userid is in the list of valid ones we generated earlier
userCondition.Values = userIdList.Select(s => s.ToString()).ToArray(); //Flip the GUID's to strings (seems that CRM likes that) then set them as the values we want to evaulate
//OK - so now we have this userCondition where valid records have their new_userid value in a collection of ID's we specify
ConditionExpression dateWeekBound = new ConditionExpression();
dateWeekBound.AttributeName = "new_availabilityday";
dateWeekBound.Operator = ConditionOperator.ThisWeek; //ConditionOperator has a whole bunch of convenience operators to deal with dates (e.g. this week, last X days etc) - check them out as they are very handy
/*
* As an aside, if we didn't want to use the convenience operator (or if none was available) we would have to create a ConditionExpression like:
*
* ConditionExpression dateLowerBound = new ConditionExpression();
* dateLowerBound.AttributeName = "new_availabilityday";
* dateLowerBound.Operator = ConditionOperator.OnOrAfter;
* dateLowerBound.Values = new object[] { <Your DateTime object here> };
*
* And a corresponding one for the upper bound using ConditionOperator.OnOrBefore
*
* Another alternative is to use ConditionOperator.Between. This is flexible for any sort of data, but the format of the Values array will be something like:
* ce.Values = new object[] { <lower bound>, <upper bound> };
*/
fe.Conditions = new ConditionExpression[] { userCondition, dateWeekBound }; //Add the conditions to the filter
qe.Criteria = fe; //Tell the query what our filters are
req.Query = qe; //Tell the request the query we want to use
RetrieveMultipleResponse resp = svc.Execute(req) as RetrieveMultipleResponse;
if (resp != null)
{
BusinessEntity[] rawResults = resp.BusinessEntityCollection.BusinessEntities;
List<DynamicEntity> castedResults = rawResults.Select(r => r as DynamicEntity).ToList();
foreach (DynamicEntity result in castedResults)
{
Property user = result.Properties.Where(p => p.Name == "new_userid").FirstOrDefault();
Property hours = result.Properties.Where(p => p.Name == "new_hours").FirstOrDefault();
if (user != null)
{
LookupProperty relationshipProperty = user as LookupProperty; //Important - the relationship attribute casts to a LookupProperty
Console.Write(relationshipProperty.Value.Value.ToString() + ", ");
}
if (hours != null)
{
CrmNumberProperty hoursAsCrmNumber = hours as CrmNumberProperty; //We also have CrmFloatProperty, CrmDecimalProperty etc if the attribute was of those data types
Console.Write(hoursAsCrmNumber.Value.Value);
}
Console.WriteLine();
}
}
#endregion
Console.ReadLine();
}
static CrmAuthenticationToken GetToken()
{
CrmAuthenticationToken token = new CrmAuthenticationToken();
token.AuthenticationType = 0; //Active Directory
token.OrganizationName = "DevCRM";
return token;
}
So..What Was That?
I'm not going to do a blow-by-blow, but home in on the key points:
The key method when using the service is the Execute() method where we pass it a request object and get back a response object. The requests will all be objects of class <Operation>Request and responses will be objects of class <Operation>Response.
You typically want to work with DynamicEntity - the <Operation>Request objects will typically expose a property called ReturnDynamicEntities which you should be setting to true
Most <Operation>Request objects have a ColumnSet property where you can specify what attributes you want returned. It is typically bad practice to specify AllColumns() and instead you should be explicit about what data you want returned. Attributes need to match their names in CRM (so of the form <prefix>_<field name>) and all in lower case
Getting users in a team isn't too interesting as it's a predefined operation in CRM and nothing special...in this case the SDK is your friend as it'll show you how these work
Retrieving a bunch custom entities is a more interesting use case and we can normally get these out by using the RetrieveMultipleRequest and RetrieveMultipleResponse methods (if you only want one record then you can just use RetrieveRequest and RetrieveResponse...but you need to know the GUID of what you are looking for to feed into the RetreiveRequest object).
For RetrieveMultipleRequest we feed it a query (QueryExpression) which says what entity(EntityName) we want to get multiple of, the attributes(ColumnSet) of that entity we want to return and the filter(Criteria) which is used to select the actual records we want
Focus on the usage of QueryExpression, FilterExpression and ConditionExpression. An important thing to know is what operators you have available for you in ConditionExpression - I have tried to call some out in the code, but once again the SDK is your best friend to know what is available
Something I haven't covered is more complex filtering like (x OR y) AND z. There is a reasonably good example here. It's just a different way to use FilterExpression and ConditionExpression
Note that RetrieveMultipleResponse contains an array of BusinessEntity. The BusinessEntity by itself is pretty useless so we cast that to a list of DynamicEntity - LINQ is really your friend here and for quite a bit of messing with CRM stuff, LINQ comes in handy
Note how we check properties - de.Properties.Where(p => p.Name == "systemuserid").FirstOrDefault(); and then check if it is NULL. This is because if in CRM an attribute of a record is NULL it won't be returned from the service call - so just because you request an attribute in the ColumnSet don't make the automatic assumption it is there (unless you have it set as mandatory in CRM - then probably OK)...test it and your app will be a whole lot less brittle.
The Property class itself has limited value - to really work with a property you have to cast it to the thing it actually is. I keep harping on about it but the SDK will tell you what the types are but it starts to feel natural after a while, e.g. the GUID for the record is in a KeyProperty, ints are in CrmNumberProperty, floats are in CrmFloatProperty, strings are StringProperty etc. Note the weak typing (which I mentioned previously) where we have to get properties by name, cast to the proper value etc
Other Takeaways
Normally you're going to have to be quite chatty with the service calls - from what I've seen this is fairly normal when developing against CRM (I can only talk about my own experience though)
It is important to be really defensive in how you code - check that properties exist, check you're casting to the right types etc.
If you have to catch an exception it'll be a SoapException and the info you typically want will be in the Detail property - very important to remember this or you'll look at the exception and think it's not telling you a whole bunch
Consult back with the customizations in CRM to figure out the name of relationship attributes, data types etc. I like to have the customizations window for the entities I need open while doing dev for easy reference.
FetchXML is really powerful but really fiddly. If you get good at it though then you'll get a lot of good mileage - a tool like this is useful. Also, a handy trick - if you can construct what you want (or an example of what you want) as an advanced find through the CRM UI then you can use this trick to get the FetchXML it used....you'll probably need to tweak the GUIDs and the like but it gives you a building block if you want to use FetchXML in your code as most of the query is written for you.
Depending on your deployment environment, you might have to mess around with what credentials are used, whether you go through a proxy etc...typical web reference stuff. Just worth noting that CRM isn't immune to that kind of thing - I don't have any real advice here, just a note as it has caused me some "fun" in the past
Related
For a while now I have been looking for a way to filter elements by their family. For example, I want all of the elements that are of family Junction Boxes - Load. I am somewhat new to the Revit API, and I do know about category filters and even a family instance filter. But I do not understand how I get the family symbol for the Junction Boxes - Load family, for example. Is there even a good way of doing this? Or am I better off filtering specific Types within the family? If so, how would I go about doing this?
Jacob,
Jeremy's answer is the right one. Here's an example of code that could be used to get all Family Instances of certain Family name:
The efficient way that Jeremy mentioned would be to use the Parameter Filter:
var famType = new FilteredElementCollector(m_doc)
.OfClass(typeof(Family)) // This is called a class filter
.FirstOrDefault(x => x.Name == "YourFamilyName");
if (famType != null)
{
const BuiltInParameter testParam = BuiltInParameter.ELEM_FAMILY_PARAM;
var pvp = new ParameterValueProvider(new ElementId((int)testParam));
var fnrv = new FilterNumericEquals();
var ruleValId = famType.Id;
var paramFr = new FilterElementIdRule(pvp, fnrv, ruleValId);
var epf = new ElementParameterFilter(paramFr);
var results = new FilteredElementCollector(m_doc)
.OfClass(typeof(FamilyInstance))
.WherePasses(epf)
.ToElements();
}
The less efficient way would be to do this:
var result = new FilteredElementCollector(m_doc)
.OfClass(typeof(FamilyInstance))
.Cast<FamilyInstance>()
.Where(x => x.get_Parameter(BuiltInParameter.ELEM_FAMILY_PARAM).AsValueString() == "YourFamilyName");
I know that Jeremy mentioned that the second method would be less efficient, but I am personally not entirely sure. The ElementParameterFilter is a Slow Filter which expands all of the elements in memory anyways. Event though it's coupled with two Quick Filters for Family and FamilyInstance selection, that's still a considerable overhead as opposed to the more direct approach that you can take using LINQ.
You also asked about being able to select FamilySymbol
var famType = new FilteredElementCollector(m_doc)
.OfClass(typeof(FamilySymbol))
.FirstOrDefault(x => x.Name == "YourFamilyTypeName");
Welcome to Revit programming!
The most effective way to get started learning and programming with the Revit API, including installation and use of the SDK, is to work through the getting started material, especially the step-by-step instructions provided by the DevTV and My First Revit Plugin video tutorials:
http://thebuildingcoder.typepad.com/blog/about-the-author.html#2
To answer your question: you use filtered element collectors to retrieve elements from the Revit database:
http://www.revitapidocs.com/2017/263cf06b-98be-6f91-c4da-fb47d01688f3.htm
You can apply many filters to them. In your case, you can directly filter for FamilySymbol instances.
There are two ways to limit the search further to retrieve only those you are interested in, named "Junction Boxes - Load":
Efficient: use a parameter filter
Simple: use .NET post-processing or LINQ
More details are provided abundantly by The Building Coder in the topic group on Retrieving Elements Using FilteredElementCollector.
In ADO.Net I created a DAL file that took parameters from the user and if they were not null, the query got extended based on if statements. Now I want to do the same in Entity Framework.
I have searched a number of sites including Stack Overflow but failed to get my satisfactory answer.
for Reference, the following link also could not help me
Select Query with Where condition depending on list values in asp.net
the required scenario is
cmd.text = "SELECT FROM tbl_name WHERE id>0 "
if(param_value != null)
{
cmd.text += " AND (param_name = '#param_value')";
if(!cmd.contains("param_name"))
cmd.parameters.addwithvalue("param_name", #param_value);
cmd.parameters["param_name"] = #param_value;
}
// proceed further with cmd.text
please ignore the syntax right now, I just wanted to convey my concept of what I want to do.
I want to apply the same concept for Entity Framework
Well two days back I found a scenerio in wheich the query (text) was built in an aspx.cs file and it was passed as it is to a custom built function in DAL which passed the text to cmd.text and processed the rest of retrieval in an ADO.net style.
This method is potentially dangerious as anyone with a bit knowlege can break this security down to grounds.
I want to create a query that has parameters as well as its vales like I have shown in above code block.
Using LINQ-to-SQL:
var param_value = 0;
db.tbl_name.Where(x => x.id > 0).Where(x => x.some_property == param_value).ToString();
If you look at the generated SQL, you'll see that's its parameterized (it picks the param_names, though).
I added the ToString() at the end, just so you could see the resulting SQL; based on OP, I'd say to leave this off, and keep modifying the query further directly in LINQ, rather than converting to SQL as a string and concatenating.
I just found out working with Entity framework is a totally different world that classic approach. In here, we work with models/objects and their relationships with each other and we access them based on that relationship. So to answer the question we need to get that model first like
Movie movie = db.Movies.Find(id);
and than from that, we get a model object which does have different properties in it like title, IMDb, rating etc.
We get them repeatedly using where clause as below:
db.Where(movies=>movies.IMDb==10).Where(movies=>movies.title=="The Game Plan")
this all is equal to the following in classic approach
AND (IMDb = 10) AND (title = 'The Game Plan')
following that, one can extend his query as much as he likes.
Again ignore the syntax here because I am here t convey the idea only.
For reference, the following links might be helpful keeping in mind the context i have explained.
Multiple where conditions in EF - StackOverflow
we are developing a framework that through the one url, generate a query based on the mapped entities by entity framework.
We are using the Dynamic Library ( http://lcs3.syr.edu/faculty/fawcett/handouts/CoreTechnologies/CSharp/samples/CSharpSamples/LinqSamples/DynamicQuery/Dynamic%20Expressions.html) and we are struggling to return to the fields of a relationship 1..N.
Example:
TB_PEOPLE > TB_PHONE
Based on this relationship, I need to accomplish the same idea of following linq:
var sql = from p in context.SomeTable
select new {
NAME = p.NAME,
PHONES = p.TB_PHONE.Select(ph => ph.PHONE)
};
Since I'm not working with typing, we chose to use dynamic library because apparently allowed us the flexibility to manipulate strings to return.
Then following the same idea , we set up the following line:
var sql = context.SomeTable.Select("new (TB_PEOPLE.TB_PHONE.PHONE)");
In this case , the returns an error stating that the attribute "PHONE" does not exist "TB_PEOPLE" of course ! So much so that we try to say that this attribute belongs to the table "TB_PHONE" but he does not understand.
So I ask you, how do I return to only certain fields of an entity where the relationship can be N? Also tried to call the method "Select":
var sql = context.SomeTable.Select("new (TB_PEOPLE.TB_PHONE.Select(PHONE))");
...there but I am informed that this method can not be used.
I do not know what else to do, any help will be appreciated!
Thank you.
I am using two different ways to implement an advanced search where each approach has their advantages and disadvantages.
The user can perform an advanced search using 3 parameters (name, familyname, and mobile).
The first way I tried is to check which of those parameters are provided by the user; that requires me to do 8 if & else checks (2 ^ number of parameters) and in each conditions I write a separate query which accepts the corresponding parameters, for example if the user has entered name & family name the where clause of the query will look like this:
where(x=>x.name.contains(name) && x.familyname.contains(familyname))
or in another case if the user has entered only the mobile the where clause of the query will look like this :
where(x=>x.mobile==mobile)
the advantage of this way is that I hit the databse once but the disadvantage is that I have to write a lot more code.
The second way I tried is that I declared an IQueryable<> object and I feed data into It with no condition at first and then I check search parameters one by one and if any of them has value I filter the IQueryable with that value and at last I perform a .ToList(). This way has the advantage of much less code but hits the database twice which is a problem.
Here is a code sample of the second way:
List<ShoppingCardForGridView> list_ShoppingCardForGridView =
new List<ShoppingCardForGridView>();
IQueryable<ShoppingCardForGridView> outQuery =
from x in db.ShoppingCards
orderby x.TFDateBackFromBank descending
where x.TFIsPaymentCompleted == true
select new ShoppingCardForGridView
{
Id = x.Id,
TFCustomerFullName =
x.Customer.TFName + " " + x.Customer.TFFamilyName,
TFDateBackFromBank = x.TFDateBackFromBank.Value,
TFIsDelivered = x.TFIsDelivered,
TFItemsPriceToPay = x.TFItemsPriceToPay,
TFDateBackFromBankPersian = x.TFDateBackFromBankPersian
};
if (!string.IsNullOrEmpty(CustomerFullName))
{
outQuery = outQuery.Where(x =>
x.TFCustomerFullName.Contains(CustomerFullName));
}
if (!string.IsNullOrEmpty(IsDelivered))
{
bool deliveryStatus = Convert.ToBoolean(IsDelivered);
outQuery = outQuery.Where(x => x.TFIsDelivered == deliveryStatus);
}
list_ShoppingCardForGridView = outQuery.ToList();
I wonder if there is any better way or a best practice to perform an advanced search using entityframework?
You're not hitting the database multiple times with your second solution. Remember that an IQueryable object is a query itself, not the results of a query. Execution is deferred until the query is actually iterated. Conditionally appending multiple Where clauses based on various if checks is changing the query itself, not processing the results of the query (since there are no results at that point in time).
You can use a profiler on your database to verify that only one query is being executed, and that the one query contains all of the filtering in that one go.
This is a reasonably common pattern; I've used it on a number of instances for making custom search pages.
Well my requirement is to do client side paging. i.e return a set of records based on the values($top, $skip) given by client. But based on my below code, am able to use only filter keyword and top or skip.
[HttpGet]
public PageResult<PersistedUser> GetAllUsers(ODataQueryOptions options)
{
TableServiceContext serviceContext = tableClient.GetDataServiceContext();
serviceContext.IgnoreResourceNotFoundException = true;
CloudTableQuery<PersistedUser> users = serviceContext
.CreateQuery<PersistedUser>(TableNames.User)
.AsTableServiceQuery();
IQueryable<PersistedUser> results = options
.ApplyTo(users.AsQueryable()) as IQueryable<PersistedUser>;
// manipulate results. Add some calculated variables to the collection etc
return new PageResult<PersistedUser>(results, null, 0);
}
I am not really sure if this is the correct way to do it as well. But my basic requirement is that I have a huge db, but i just need to return a small set of entities at a time in an efficient time. I would really appreciate if someone could provide some code snippets.
I'm using the same way and it's working fine.
Few differences:
I have a service layer that expose my entites. In my services I return IQueryable and apply the O Data filter.
[AuthKey]
[GET("api/brands/")]
public PageResult<BrandViewModel> GetBrands(ODataQueryOptions<Brand> options)
{
var brands = (IQueryable<Brand>)options.ApplyTo(_brandServices.FindBrands());
return new PageResult<BrandViewModel>(BrandViewModel.ToViewModel(brands), Request.GetNextPageLink(), Request.GetInlineCount());
}
Here's an updated version of your code that uses the generic version of ODataQueryOptions and applies the $top and $skip options for paging.
[HttpGet]
public PageResult<PersistedUser> GetAllUsers(
ODataQueryOptions<PersistedUser> options)
{
TableServiceContext serviceContext = tableClient.GetDataServiceContext();
serviceContext.IgnoreResourceNotFoundException = true;
CloudTableQuery<PersistedUser> users = serviceContext
.CreateQuery<PersistedUser>(TableNames.User)
.AsTableServiceQuery();
IQueryable<PersistedUser> results = options.ApplyTo(users);
int skip = options.Skip == null ? 0 : options.Skip.Value;
int take = options.Top == null ? 25 : options.Top.Value;
return new PageResult<PersistedUser>(
results.Skip(skip).Take(take).ToList(),
Request.GetNextPageLink(),
null);
}
When planning your OData model, separate it from the underlying storage model. In some domains it may be possible to exposed groups and then use navigation properties to access the members of the group.
For example, suppose you have a booking system. You may store your bookings in a long, long table by date-time.
But you could potentially expose the OData model by grouping into collections of year and week.
http://service.net/odata/year(2013)/week(22)/bookings
In your controller, you'd compose a Table Storage range query from the temporal parameters supplied.
If there were >1,000 bookings, but not a huge number more, you could page through them server-side, exhaust the set, and deliver all the bookings back to the OData client, or sort them and allow IQueryable over this set. See note, bottom.
This would afford the OData consumer a natural mechanism for filtering data while keeping result set sizes low. Where there are many bookings per week, it could be sub-grouped further by day of week and hour.
This is completely theoretical, but I think OData v4 and its containment feature would allow such URLs to be routed and the relationship described so as to produce correct metadata for OData consumers like Excel.
http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/odata-containment-in-web-api-22
Note in the above sample code that they create an arbitrary route/URL for counting the contained items, so it seems flexible.
If nested containment is not allowed, then alternatively, consider a BookingRange entity in the OData EDM with Year and Week properties to allow:
http://service.net/odata/bookingrange(2013,22)/bookings
Another idea I've considered is calculating a page number on insert. That is, to have a PageNumber on the TS entity itself and use an algorithm to assign a page number to it. This is essentially the same as generating a good partition key but with many pages/partitions.
A table expecting to hold 200m rows could have 1m pages (generate a large psuedo random number and Mod it by 1m), each with 200 items. At first, most pages would be empty, so you'd need to write a page mapper algo that alters as the row count grows. "Page" 1 maps to page range 0000001 - 0000100, etc.
As you can see, this is getting complex, but its essentially the same system that Azure uses itself to auto-balance your data across partitions and spread those partitions across its nodes.
Ultimately, this would again need a way for the "Page" number to be specified in the URL. Finally, each page would contain varying numbers of items, depending on the distribution of the algorithm used.
Note - I think the reason that TS does not support top and skip, or skip, is that the order of the rows returned cannot be guaranteed and there is no ordering mechanism within TS (which would be a big burden). Therefore, pages collated from top and skip would contain a "random" bag each time.
This means that my suggestion above to offer paging over a subset/group of data, requires that the entire subset is brought into the service tier and a sort order applied to them, before applying the top and skip, though it could be argued that the client should understand that top/skip without an order is meaningless and the onus is on them to send the right options.