October 2006 - Posts

Writing A CRM Callout Assembly: A How To

I was recently asked to write a callout assembly to be attached to a client's CRM workflow service.  A callout assembly (in this regard) is an assembly that is attached to the CRM workflow service and is sort of like an external event handler.  So when someone adds/updates/deletes records in CRM, the callout assembly is fired.  I had never done it before, but I thought it wouldn't be too much of a problem.  There are some samples in the CRMSDK and I was sure that searching on Google and MSDN would yield the answers to any questions I'd have.  I was wrong.

There isn't too much help in either of those two sources, as I found out the hard way.  This possibly isn't the most common thing in the world to do, I'm guessing.

The problem I had with the "readme"s in the callout samples given in the CRMSDK is that they just blindly say "do this" and "do that," without any explanation of what's going on.  They also missed a step, as far as I can see.  They don't explain the callout.config.xml file at all.  I'll attempt to explain things, step-by-step, since I don't really see a definitive explanation of how these callout assemblies work anywhere (I've seen similar posts on other blog sites with less information, however).

Step 1: create a blank solution with a class library project inside.

Step 2: add a web reference to your CRM web service.  It should look something like this: http://<yourserver>/mscrmservices/2006/CrmService.asmx, as the readme got correct.  If you need to login, you better have all that information handy, as you'll need it later.  This information will include: a) user name b) password c) domain d) CRM server.

Step 3: add a reference to the Microsoft.Crm.Platform.Callout.Base.dll file.  If you need to download that, you should do so.  I had to.  This is necessary for the next step.

Step 4: create a class for your callout that extends the CrmCalloutBase class.  You will need the dll from the previous step to do this.  Make sure this class has "using Microsoft.Crm.Callout;" in the using directives for the base class and "using <yourassemblyname>.<yourwebreferencename>;" for the web service.  If you are ever going to need to connect to the web server (say, if a child object is updated/added/deleted, the parent needs to update its information, for example), then you'll need the user name, password, domain and CRM server information from before.  You'll need this code to access the CRM web service:

CrmService service = new CrmService();
service.Url = <CRM_SERVER> + "crmservice.asmx";
service.PreAuthenticate = true;
service.Credentials = new NetworkCredential(<CRM_USER>, <CRM_PASSWORD>, <CRM_DOMAIN>);
WhoAmIRequest userRequest = new WhoAmIRequest();
WhoAmIResponse userResponse = (WhoAmIResponse)service.Execute(userRequest);

And your service is now ready to use.  I.e. if a child's information is edited, you may want to use the web service to update the parent.  This may be the whole reason for creating a callout assembly.

Step 5: decide what events you want to listen into.  Do you want to react to whenever a case is submitted?  Do you want to react to whenever someone updates their contact information?  Do you want to check submitted information before it gets submitted?  Whatever it is, there are plenty of things to listen into.  To find out what you can listen to, inside your class, start a new line (with intellisense enabled) and type override and then a space to see what possibilities you have.  For quick reference, there's pre- and post-:

  • Create
  • Update
  • Delete
  • Assign
  • Set state
  • MergePersonally
  • PreSend
  • PostDeliver

Step 6: implement the code you want to fire when your event is triggered.  You can override a bunch of methods (all of them, actually) in the same callout class, so feel free.  I overrode three in mine.  I don't quite understand why the entity context is passed, as you can set your event to only listen for when an event is fired for an entity of a particular type.  I personally don't have any code in my callout I just wrote that worries about what type of entity I'm dealing with.

Step 7: create the callout.config.xml file.  Assuming you're using CRM 3.0, you should have this line of code as your second line of code instead of the ones in the CRMSDK samples: "<callout.config version="3.0" xmlns=" http://schemas.microsoft.com/crm/2006/callout/">" or your config file might not be recognized.  Inside the callout config file, you'll need a <callout> node for each event/entity combination you are listening into that looks like this: <callout entity="myentityname" event="Post/Pre<see above bulleted list for ideas>"> with an optional onerror property with the value of "abort" or "ignore."  Inside this node, you'll need a subscription node that looks something like this: <subscription assembly="myassembly.dll" class="myfullnamespace.myclass(from step four)">.  Inside this node, you'll need one or more <postvalue> nodes IF you are listening in on a "Post" event (not a "Pre" event).  Each of these <postvalue> nodes needs to have the name of a property of the entity you are going to work with in your code.  For example, if you working with an incident entity and want to look at the ticket number in your callout assembly's class, you'll need this inside your subscription node: <postvalue>ticketnumber</postvalue>.  One short cut, if you're going to use a lot of properties, or an expanding number of properties (in case your assembly will be modified as time goes on) or if you're just really lazy is to put this in your subscription: <postvalue>@all</postvalue>, and you'll then get all of the properties.  That's what I did.  Mainly because I knew that the objects don't have a lot of properties.  I would also suggest writing to a log file throughout your code so you can debug more easily.

Step 8: compile your code and attach it.  Attaching it is pretty tricky, so make sure you pay close attention.  I am not sure you'll need to do all of these things, but I did, so here's what should work for sure.  First, go into the services console (of the CRM server...) and stop the Microsoft CRM Workflow Service.  Next, if you've already deployed a previous version of your callout assembly, RENAME IT.  It won't let you delete it, but you can rename it...  Then, move (the new version of) your callout assembly to the \Program Files\Microsoft CRM\Server\bin\assembly folder.  Then, restart IIS.  I'll wait...  Ok, now start the Microsoft CRM Workflow Service.  Delete the old assembly (the one you renamed) if this isn't the first time you deployed it.  IF YOU MAKE ANY CHANGES TO THE DLL OR THE CALLOUT.CONFIG.XML FILE(S), YOU MUST REPEAT STEP 8 OR IT WON'T TAKE EFFECT!

That should do it!  Now, if you have trouble getting into the workflow or think it's too restrictive/not powerful enough, you can write C# code (or VB code, if you are so inclined) that can do whatever you want and then just attach it to the workflow process using the process outlined above.  Happy calling out!

CRM SDK Bug - How to upload files into CRM 3.0

I wanted to attach a file to a CRM case created outside of CRM using a web service.  In order to do this, you have to attach something called an "annotation" to something called an "incident."  An incident is a case and an annotation is anything that would fit under the "Notes and Article" tab.

After creating the annotation, you then have to attach the file's data to something called an "UploadFromBase64DataAnnotationRequest."  Wonderful name for an object, by the way.  Then you attach the UploadFromBase64DataAnnotationRequest object to the annotation you just made and have the service execute this UploadFromBase64DataAnnotationRequest object.  All very, very, very intuitive...

The code being thrown around the Internet to do this is as follows:

string data;// Variable declared outside of using statement.
 
// Create an instance of StreamReader to write text to a file.
// The using statement also closes the StreamReader.
using (StreamReader sr = new StreamReader("temp.txt"))
{
   // Read in the contents of the file.
   TextReader reader = sr;
   data = reader.ReadToEnd();
}
 
// Encode the data using base64.
byte[] byteData = new byte[data.Length];
byteData = System.Text.Encoding.UTF8.GetBytes(data);
string encodedData = System.Convert.ToBase64String(byteData);
 
// Create the request object.
UploadFromBase64DataAnnotationRequest upload = new UploadFromBase64DataAnnotationRequest();
 
// Set the properties of the request object.
upload.AnnotationId = annotationId;
upload.FileName = "temp.txt";
upload.MimeType = "text/plain";
upload.Base64Data = encodedData;
 
// Execute the request.
UploadFromBase64DataAnnotationResponse uploaded = (UploadFromBase64DataAnnotationResponse) service.Execute(upload);

I tried this and it worked... for text files and HTML files.  When I tried to upload a pdf, ppt, doc, etc., it failed.  Turns out there's more than one MimeType and more than one way to encode things!

After reading a very interesting and informative article on different encodings, located here: http://www.joelonsoftware.com/articles/Unicode.html I was no closer to actually solving my problem.  I did figure out while dinking around in the code that the MimeType can be dynamically determined by this line of code:

upload.MimeType = fileUpload.PostedFile.ContentType;

So that part was solved.  The other part was a tad more complicated.  I opened a pdf file in notepad to find out what the encoding was and after searching for "encoding" in the gobbledy-gook that was the Notepad interpretation of the pdf, I found this: "WinAnsiEncoding."  I looked in the System.Text.Encoding namespace and couldn't find this encoding type!  In the aforementioned article, it's mentioned that there are hundreds of encoding types.  But the System.Text.Encoding namespace has six... Yes, six.  Not counting "Default."  Not very multi-lingual if you ask me.  I tried various ways of not encoding it, but that usually worked out WORSE than before, if at all.  After speaking with a co-worker, Justin, he showed me something he did that worked in a similar situation.  And after tweaking it for my scenario, this is the solution that worked:

byte[] selectedFile = null;

try
{
   if (postedFile != null)
   {
      byte[] rawFile = new byte[postedFile.InputStream.Length];
      postedFile.InputStream.Read(rawFile, 0, (int)postedFile.InputStream.Length);

      selectedFile = rawFile;
   }
   else
      selectedFile = null;
}
catch (Exception)
{
   selectedFile = null;
}
finally
{
   if (postedFile != null)
      postedFile.InputStream.Close();
}

String encodedData = Convert.ToBase64String(selectedFile);

// Create the request object.
UploadFromBase64DataAnnotationRequest upload = new UploadFromBase64DataAnnotationRequest();

// Set the properties of the request object.
upload.AnnotationId = annotationID;
upload.FileName = postedFile.FileName;
upload.MimeType = postedFile.PostedFile.ContentType;
upload.Base64Data = encodedData;

// Execute the request.
UploadFromBase64DataAnnotationResponse uploaded = (UploadFromBase64DataAnnotationResponse)service.Execute(upload);

The person who wrote the original block of code that I copied into this post above has actually issued somewhat of an apology since he wrote that for his company to deal with text files and never meant it to be used as liberally as it has been used across the Internet.  You can find his apology and his unfinished solution to the problem that I completed above (and tweaked to fit my situation) here: http://www.invokesystems.com/cs/blogs/mscrm/archive/2006/01/25/8.aspx

Posted by vbullinger | with no comments

MaxRequestLength Exceeded Problem

A while ago, I was asked to change the maximum size of an uploaded document in a web application to 20 MB.  The default for any application is 4 MB, if I recall correctly.  I did that, simply enough, by changing the "maxRequestLength" attribute of the "httpRunTime" tag to 20000 (it's in KB).

The problem with that is that when someone tries to upload a document that exceeds the maxRequestLength value, you get just about the worst error I've ever encountered.  First, it flashes that dreaded screen with the yellow background and big, maroon text at the top saying that there was a server error that wasn't handled, etc.  It says that the maximum request length has been exceeded.  Worse yet, it only flashes that error message for a second at most.  Then it redirects you to a page not found error.  This was unacceptable.

I tried to see if there was an error code I could catch and then have a page to which to redirect the user.  No dice.  Then I tried to catch the error in the control.  Again, no luck.  So I tried to catch it on application_error in the global asax file.  It caught it!

So I decided I'd redirect them from there.  No luck again!  It didn't matter that you could catch it, you couldn't handle it, it seemed.  Telling it to redirect and kill the current request did nothing.  It just didn't care!  I was able to catch it at application_beginrequest as well, but it also didn't care.  As long as the maxRequestLength is exceeded, you're in trouble.  You can't do anything about it.

I Googled it for a long time.  I tried using httpModules and a million other ways around it.  The point was to find a solution I could put on inetium's Wiki -  http://wiki.inetium.com/default.aspx/InetiumWiki/Inetium%20Wiki%20Home.html - so we could have this solution forever.  I never found an adequate answer.

It seems that, although I didn't find any definitive information on this, it's a security issue.  As in, you can't allow someone to get as far as your application with this gigantic file.  People could hack in and use your server for "warez" or whatever.  So you can't handle that issue.  I'd like to know a little bit more about the problem, however.

My best solution was to set the maxRequestLength a little higher than I wanted to avoid getting that ugly error unless the file is just too massive and use an appSetting to do the actual restricting at 20 MB.  This way, I can catch it and display a friendly message on the same page (not even redirecting) that just says to pick a smaller file, that kind of thing.  Also, I get the security if someone tries to submit a giant file, like a movie.

Does anyone have a better solution?  Thoughts?  Insights?  Deeper knowledge of the problem?

Posted by vbullinger | 1 comment(s)