Saturday, March 27, 2010

Compilation failed. Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.

If you've recently upgraded FIM to RTM and you're getting the following error message when you compile your custom activities, then you probably need to copy the new Microsoft.IdentityManagement libraries out of the GAC.

"Compilation failed. Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information."

copy /y C:\Windows\assembly\GAC_MSIL\Microsoft.IdentityManagement.WFExtensionInterfaces\4.0.2592.0__31bf3856ad364e35\Microsoft.IdentityManagement.WFExtensionInterfaces.dll .

copy /y C:\Windows\assembly\GAC_MSIL\Microsoft.IdentityManagement.Activities\4.0.2592.0__31bf3856ad364e35\Microsoft.IdentityManagement.Activities.dll .

copy /y C:\Windows\assembly\GAC_MSIL\Microsoft.IdentityManagement.WebUI.Controls\4.0.2592.0__31bf3856ad364e35\Microsoft.IdentityManagement.WebUI.Controls.dll .

Monday, March 22, 2010

Query/enumerate all declarative workflow templates in an SPWeb and programmatically read the XOML files

It took me all day to figure this one out. You'd think something like this could be done through the object model, but it's not available in WSS. Here's a complete copy of my console app, with all trial/error code left in. I hope this helps someone out there!

Edit: Sorry, I left out some details. What can I say; I was a little weary after a grueling battle with Sharepoint. Anyway, the workflows I'm talking about here are declarative Sharepoint workflows, meaning that you assemble them in Sharepoint Designer.

Also, I was able to read the XOML files through the object model, but I wouldn't call it the most direct route. You'd think that you could read the workflow templates through the object model, but they don't show up in SPWeb.WorkflowTemplates, SPWorkflowAssociation.BaseTemplate is null (probably because it's declarative), and there is no Workflow.asmx service in WSS (not sure if that would even suffice).

So, what I ended up doing was getting the workflow name from the SPWorkflowAssociation (from the SPList), and then assembling a URL in the form of:

/Workflows/wfName/wfName.xoml

Then, I get a handle on the XOML as an SPFile, and from there I can read it into an XmlDocument.


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using Ensynch;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;

namespace EnumerateSPWorkflowTemplates
{
class Program
{
private static string webUrl =
"http://ens-ilm01/sites/devsandbox/spworkflow/";

static void Main(string[] args)
{
try
{
using (SPSite site = new SPSite(webUrl))
using (SPWeb web = site.OpenWeb())
{
foreach (SPWorkflowTemplate wf in web.WorkflowTemplates)
{
Console.WriteLine(EnsynchTools.PrintHeading(
"Workflow Template: " + wf.Name));
//Console.WriteLine(wf.Xml);
writeXml(wf.Xml);
}
foreach (SPList list in web.Lists)
{
foreach (SPWorkflowAssociation association in list.WorkflowAssociations)
{
//SPWorkflowTemplate wf = association.BaseTemplate;
//if (wf != null)
//{
Console.WriteLine(EnsynchTools.PrintHeading(
"Workflow Association: " + association.Name));
//Console.WriteLine(association.SoapXml);
writeXml(association.SoapXml);
readWorkflowTemplate(association);
//}
}
}
crawlFolders(web, "Workflows");
}
}
catch (Exception exc)
{
Console.WriteLine(EnsynchTools.ExceptionDetails(exc));
}

Console.Write("Press enter to exit...");
Console.ReadLine();
}

private static void writeXml(string xml)
{
XmlDocument doc = new XmlDocument();
doc.Load(new StringReader(xml));
writeXml(doc);
}

private static void writeXml(XmlDocument doc)
{
doc.Save(Console.Out);
Console.WriteLine("\r\n");
}

private static void readWorkflowTemplate(
SPWorkflowAssociation association)
{
XmlDocument doc = new XmlDocument();
doc.Load(new StringReader(association.SoapXml));
XmlNode node = doc.SelectSingleNode("/WorkflowTemplate");
// Assemble the URL from the workflow name.
XmlAttribute attribute = node.Attributes["Name"];
string wfName = attribute.Value.Replace(" ", "%20");
string webRelativeFolder = "Workflows/" + wfName;
string xomlFileName = wfName + ".xoml";
string xomlUrl = webUrl + webRelativeFolder + "/" + xomlFileName;
try
{
Console.WriteLine("Trying to access " + xomlUrl);

SPFolder wfFolder = association.ParentWeb.GetFolder(
webRelativeFolder);
SPFile xomlFile = wfFolder.Files[xomlFileName];
Console.WriteLine("Found file: " + xomlFile.Url);

//System.Net.WebClient oWebClient = new System.Net.WebClient();
//oWebClient.Credentials = new System.Net.NetworkCredential (
// "username","password","domain");
//String sResponseData = System.Text.Encoding.ASCII.GetString(
// oWebClient.DownloadData(xomlUrl));

doc = new XmlDocument();
//doc.Load(new StringReader(sResponseData));
using (Stream xomlStream = xomlFile.OpenBinaryStream())
{
doc.Load(xomlStream);
}
writeXml(doc);
Console.WriteLine("\r\n");
}
catch (Exception exc)
{
Console.WriteLine(exc.Message);
}
}

private static void crawlFolders(SPWeb web, string subFolder)
{
SPFolder rootFolder = web.GetFolder(subFolder);
crawlFolders(rootFolder);
}

private static void crawlFolders(SPFolder folder)
{
Console.WriteLine("Crawling: " + folder.Url);
foreach (SPFile file in folder.Files)
{
Console.WriteLine("- " + file.Name);
}
// Recursively count SPFiles in SPFolders
foreach (SPFolder subfolder in folder.SubFolders)
{
crawlFolders(subfolder);
}
}

}
}

Monday, February 1, 2010

FIM Query Tool for FIM 2010 RC1

I forgot to mention that I updated the FIM Query Tool to work with RC1 and the new unsupported web service client. So, if you haven't already stumbled upon it, you can download the latest release and take it for a test drive:

http://fimquerytool.codeplex.com/

And please leave me any feedback on the Codeplex site or here; thanks!

Error defining Activity Information Configuration: Access denied

I know the answer to this one is trivial, but if you encounter it, I thought I'd let you know that, no, you're not going crazy. Evidently, there is no default MPR for creating Activity Information Configuration's in the FIM portal.

So, if you're defining a custom workflow, and you're adding your interface to the portal for the first time, you may get the error, "Create Resource: Access denied." Here's a screen shot:



So the fix is to define an MPR to allow you to create these guys. Note that there is already an MPR that allows Administrators to read all resources, so we'll avoid the redundancy here.

Display NameAdministration: Administrators have full control over Activity Information Configuration resources
Action Parameter*
Action TypeAdd; Create; Delete; Modify; Remove
Action Workflows0
Authentication Workflows0
Authorization Workflows0
DescriptionAdministration: Administrators have full control over Activity Information Configuration resources
Grant RightTRUE
Principal SetAdministrators
Resource Current SetAll Activity Information Configurations
Resource Final SetAll Activity Information Configurations

Where is the ActivitySettingsPart?

If you're building a custom workflow/activity for FIM 2010, you may be wondering where you can find the ActivitySettingsPart (which you must override in your web interface class). As of RC1, they've made it a bit harder to find, but here's where you can find the DLL that it's in:

ClassActivitySettingsPart
NamespaceMicrosoft.IdentityManagement.WebUI.Controls
AssemblyMicrosoft.IdentityManagement.WFExtensionInterfaces
Where to find(Copy it out of the Global Assembly Cache from a DOS prompt.)

copy C:\Windows\assembly\GAC_MSIL\Microsoft.IdentityManagement.WFExtensionInterfaces\4.0.2574.0__31bf3856ad364e35\Microsoft.IdentityManagement.WFExtensionInterfaces.dll .

Update for RTM:
copy /y C:\Windows\assembly\GAC_MSIL\Microsoft.IdentityManagement.WFExtensionInterfaces\4.0.2592.0__31bf3856ad364e35\Microsoft.IdentityManagement.WFExtensionInterfaces.dll .


Additionally, forum users have suggested a couple other ways to get it:

http://social.technet.microsoft.com/Forums/en-US/ilm2/thread/ce902e07-15fe-40ef-9872-c4f8da83cf80/

While I'm at it, I'll show you where to find all of the building block activities for FIM custom workflow dev:

ClassApprovalActivity
AuthenticationGateActivity
AuthenticationWorkflow
CreateResourceActivity
CurrentRequestActivity
DeleteResourceActivity
DomainSynchronizationActivity
EmailDeliveryActivity
EmailNotificationActivity
EnumerateResourcesActivity
FilterValidationActivity
FunctionActivity
GroupMembershipValidationActivity
GroupValidationActivity
PWResetActivity
QueuedInputActivity
ReadResourceActivity
ReceiveCreateResourceActivity
RequestorValidationActivity
ResolveGrammarActivity
SequentialWorkflow
SynchronizationRuleActivity
UpdateRequestActivity
UpdateResourceActivity
XmlInteractiveActivity
NamespaceMicrosoft.ResourceManagement.Workflow.Activities
AssemblyMicrosoft.ResourceManagement
Where to findC:\Program Files\Microsoft Forefront Identity Manager\2010\Service


Enjoy!

Monday, January 25, 2010

NullReferenceException in ResolveGrammarActivity

Today I ran into a problem with the ResolveGrammarActivity. I'm not sure if it's a bug, but before I post it to the FIM forum, I thought I'd describe it here so that I'm reminded to post the solution (and to host an image).

I know this used to work in ILM 2 RC0, but in FIM 2010 RC1, I'm getting a NullReferenceException. Here's the deal; I can write a parameter to the workflow dictionary, but when I try to use that in a ResolveGrammarActivity, I get the exception.

Here's an example of adding myself to the workflow dictionary (WorkflowData):



So far, so good. Now I try passing [//WorkflowData/JoeZamora] into the grammar resolver. Here's my debug log:

2010-01-25 17:18:10,477 --6-- DEBUG [Ensynch.FIM.Workflow.Activities.ChangeAttributeActivity]

Source Class : System.Workflow.ComponentModel.Activity
Source Instance : 7. Remove Joe from Group Members
Source Method : RaiseEvent
Current user : INFO\svc.fimws

Passing these data into the ResolveGrammarActivity:
NewGrammarExpression : [//WorkflowData/JoeZamora]
NewResolvedExpression :
NewWorkflowDictionaryKey :

2010-01-25 17:18:10,535 --6-- ERROR [Ensynch.FIM.Workflow.Activities.ChangeAttributeActivity]

Source Class : System.Workflow.ComponentModel.ActivityExecutor`1[T]
Source Instance : 7. Remove Joe from Group Members
Source Method : HandleFault
Current user : INFO\svc.fimws

System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.ResourceManagement.WFActivities.Resolver.GetDisplayStringFromGuid(Guid id, String[] expansionAttributes)
at Microsoft.ResourceManagement.WFActivities.Resolver.ReplaceGuidWithTemplatedString(Match m)
at System.Text.RegularExpressions.RegexReplacement.Replace(MatchEvaluator evaluator, Regex regex, String input, Int32 count, Int32 startat)
at System.Text.RegularExpressions.Regex.Replace(String input, MatchEvaluator evaluator)
at Microsoft.ResourceManagement.WFActivities.Resolver.GetStringAttributeValue(Object attribute)
at Microsoft.ResourceManagement.WFActivities.Resolver.ResolveEvaluatorWithoutAntiXSS(Match m)
at Microsoft.ResourceManagement.WFActivities.Resolver.ResolveEvaluatorForBodyWithAntiXSS(Match m)
at System.Text.RegularExpressions.RegexReplacement.Replace(MatchEvaluator evaluator, Regex regex, String input, Int32 count, Int32 startat)
at System.Text.RegularExpressions.Regex.Replace(String input, MatchEvaluator evaluator)
at Microsoft.ResourceManagement.WFActivities.Resolver.ResolveBody(String input)
at Microsoft.ResourceManagement.Workflow.Hosting.ResolverEvaluationServiceImpl.ResolveLookupGrammar(Guid requestId, Guid targetId, Guid actorId, Dictionary`2 workflowDictionary, Boolean encodeForHTML, String expression)
at Microsoft.ResourceManagement.Workflow.Activities.ResolveGrammarActivity.Execute(ActivityExecutionContext executionContext)
at System.Workflow.ComponentModel.ActivityExecutor`1.Execute(T activity, ActivityExecutionContext executionContext)
at System.Workflow.ComponentModel.ActivityExecutor`1.Execute(Activity activity, ActivityExecutionContext executionContext)
at System.Workflow.ComponentModel.ActivityExecutorOperation.Run(IWorkflowCoreRuntime workflowCoreRuntime)
at System.Workflow.Runtime.Scheduler.Run()

Hmmm, not a very helpful message. I'm pretty sure this is a bug, because ordinarily you try to handle NullReferenceExceptions in code that's exposed to the public.

Anyway, I'll post this to the forum and get back to you.

Update: I posted this on the FIM forum to see if it got any bites:

http://social.technet.microsoft.com/Forums/en-US/ilm2/thread/39d887bf-638c-4539-8f0e-afd9c0ff4490

Joe Schulman mentioned that someone already logged a similar problem:

https://connect.microsoft.com/site433/feedback/ViewFeedback.aspx?FeedbackID=523776&wa=wsignin1.0#tabs

If you run into this same problem, please visit the Connect link and vote it as important!

Thursday, January 7, 2010

NullReferenceException in EnumerationResultEnumerator.Dispose()

Still working with the unsupported web service client for RC1, and I ran into the following error:

System.NullReferenceException

Object reference not set to an instance of an object.

at Microsoft.ResourceManagement.Client.EnumerationResultEnumerator.Dispose() in C:\FIM2010Dev\Microsoft.ResourceManagement.Samples\Microsoft.ResourceManagement.Client\EnumerationResultEnumerator.cs:line 46


The problem was pretty easy to find, but I thought I'd at least change the code to throw a more helpful exception. The problem happened because I naively tried to use the LINQ methods Count() and First() consecutively:

IEnumerable<RmResource> objects =
client.Enumerate(xpath, selection.ToArray());
if (objects != null && objects.Count() > 0)
{
string result = objects.First()[ATTRIBUTE_DISPLAY_NAME].Value.ToString();
if (!string.IsNullOrEmpty(result))
{
displayName = result;
break;
}
}

However, much like LINQ to SQL behavior, the queries are run on-the-fly as the results are enumerated, and then they're disposed. So, you guessed it, we can't enumerate the results more than once (or at least we should avoid it). You've probably seen this exception from the LINQ to SQL libraries, "The query results cannot be enumerated more than once."

Here's my modified code for the EnumerationResultEnumerator class. I'm throwing a more helpful exception with the message above. I've highlighted my changes:

using System;
using System.Collections.Generic;
using System.Xml.Schema;
using System.Text;

using Microsoft.ResourceManagement.Client.WsEnumeration;
using Microsoft.ResourceManagement.ObjectModel;

namespace Microsoft.ResourceManagement.Client
{
class EnumerationResultEnumerator : IEnumerator<RmResource>, IEnumerable<RmResource>
{
WsEnumerationClient client;
List<RmResource> results;
int resultIndex;
bool endOfSequence;
EnumerationContext context;
String filter;
String[] attributes;
RmResource current;
RmResourceFactory resourceFactory;

bool disposed = false;

internal EnumerationResultEnumerator(WsEnumerationClient client, RmResourceFactory factory, String filter, String[] attributes)
{
results = new List<RmResource>();
this.client = client;
this.filter = filter;
this.resourceFactory = factory;
this.attributes = attributes;
}

#region IEnumerator<RmResource> Members

public RmResource Current
{
get { return current; }
}

#endregion

#region IDisposable Members


public void Dispose()
{
if (!disposed)
{
this.context = null;
this.results.Clear();
this.results = null;
this.disposed = true;
}
}

#endregion

#region IEnumerator Members

object System.Collections.IEnumerator.Current
{
get { return current; }
}

public bool MoveNext()
{

if (disposed)
{
throw new InvalidOperationException("The query results cannot be enumerated more than once.");
}

lock (this.client)
{
if (resultIndex < results.Count)
{
this.current = results[resultIndex++];
return true;
}
else
{
PullResponse response;
if (this.context == null)
{
if (resultIndex > 0)
{
// case: previous pull returned an invalid context
return false;
}
EnumerationRequest request = new EnumerationRequest(filter);
if (attributes != null)
{
request.Selection = new List<string>();
request.Selection.AddRange(this.attributes);
}
response = client.Enumerate(request);
this.endOfSequence = response.EndOfSequence != null;
}
else
{
if (this.endOfSequence == true)
{
// case: previous pull returned an end of sequence flag
this.current = null;
return false;
}
PullRequest request = new PullRequest();
request.EnumerationContext = this.context;
response = client.Pull(request);
}

if (response == null)
return false;
resultIndex = 0;
this.results = resourceFactory.CreateResource(response);
this.context = response.EnumerationContext;
this.endOfSequence = response.IsEndOfSequence;
if (this.results.Count > 0)
{
this.current = results[resultIndex++];
return true;
}
else
{
this.current = null;
return false;
}
}
}
}


public void Reset()
{
if (!disposed)
{
this.results.Clear();
this.context = null;
}
}

#endregion

#region IEnumerable<RmResource> Members

public IEnumerator<RmResource> GetEnumerator()
{
return this;
}

#endregion

#region IEnumerable Members

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this;
}

#endregion
}
}

Paolo Tedesco's changes for object count in enumeration responses

As one final note, Paolo has posted code for including the object count in enumeration responses. I haven't tried it yet, but it looks like something I could have used here. Here's the link (note that I'm also posting links to my changes on this thread):

http://social.technet.microsoft.com/Forums/en-US/ilm2/thread/ffc16720-0dfb-4131-b676-9225f15b4f72?prof=required