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 calledReturnDynamicEntities
which you should be setting totrue
- Most
<Operation>Request
objects have aColumnSet
property where you can specify what attributes you want returned. It is typically bad practice to specifyAllColumns()
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
andRetrieveMultipleResponse
methods (if you only want one record then you can just useRetrieveRequest
andRetrieveResponse
...but you need to know the GUID of what you are looking for to feed into theRetreiveRequest
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
andConditionExpression
. An important thing to know is what operators you have available for you inConditionExpression
- 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
andConditionExpression
- Note that
RetrieveMultipleResponse
contains an array ofBusinessEntity
. TheBusinessEntity
by itself is pretty useless so we cast that to a list ofDynamicEntity
- 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 isNULL
. This is because if in CRM an attribute of a record isNULL
it won't be returned from the service call - so just because you request an attribute in theColumnSet
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 aKeyProperty
, ints are inCrmNumberProperty
, floats are inCrmFloatProperty
, strings areStringProperty
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 theDetail
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