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

Wednesday, January 6, 2010

Multi-valued attributes aren't multi-valued

I'm back on FIM after a brief hiatus, and I've begun updating my FIM Query Tool with the new unsupported web service client for RC1.

Today I noticed that my multi-valued attributes didn't have multiple values. I'm calling Enumerate on the DefaultClient to check the computed members of a Group. After some digging, I discovered that the attributes weren't recognized as multi-valued because I didn't refresh the schema after instantiating the DefaultClient.

Here's a code snippet from the unsupported WS client sample program:

// First need to construct the client
// We will assume all default contracts

DefaultClient client = new DefaultClient();
// We set the client credentials since often the test cases or client apps run under different accounts
client.ClientCredential = Credential.GetAdminCredential();
// We refresh the schema so that the web service put operations are better informed
client.RefreshSchema();

I interpreted the last comment as, "We only need to call RefreshSchema() when using Put operations." Since I'm only using Enumerate/Pull operations, I just left it out of my code. Well, I was wrong. Turns out that the RmFactory needs a schema refresh before it can determine whether an attribute is multi-valued.

Since this is a potential pitfall every time you use the DefaultClient, I decided to refactor its constructors. Originally, there were three constructors. I added three more that accept an additional NetworkCredential, and now they all call RefreshSchema(). Of course, if you use the original constructors (without the NetworkCredential), they'll use the caller's credentials. Since the FIM Query Tool is a Windows app, it'll use your credentials.

Here are the refactored constructors (note the additional helper method):

public DefaultClient() : this(null)
{
}

public DefaultClient(NetworkCredential clientCredential)
{
this.wsTransferClient = new WsTransferClient();
this.wsTransferFactoryClient = new WsTransferFactoryClient();
this.wsEnumerationClient = new WsEnumerationClient();
this.mexClient = new MexClient();

this.resourceFactory = new RmResourceFactory();
this.requestFactory = new RmRequestFactory();

init(clientCredential);
}

public DefaultClient(
String wsTransferConfigurationName,
String wsTransferFactoryConfigurationName,
String wsEnumerationConfigurationName,
String mexConfigurationName
) : this(
null,
wsTransferConfigurationName,
wsTransferFactoryConfigurationName,
wsEnumerationConfigurationName,
mexConfigurationName
)
{
}

public DefaultClient(
NetworkCredential clientCredential,
String wsTransferConfigurationName,
String wsTransferFactoryConfigurationName,
String wsEnumerationConfigurationName,
String mexConfigurationName
)
{
this.wsTransferClient = new WsTransferClient(wsTransferConfigurationName);
this.wsTransferFactoryClient = new WsTransferFactoryClient(wsTransferFactoryConfigurationName);
this.wsEnumerationClient = new WsEnumerationClient(wsEnumerationConfigurationName);
this.mexClient = new MexClient(mexConfigurationName);

this.resourceFactory = new RmResourceFactory();
this.requestFactory = new RmRequestFactory();

init(clientCredential);
}

public DefaultClient(
String wsTransferConfigurationName,
String wsTransferEndpointAddress,
String wsTransferFactoryConfigurationName,
String wsTransferFactoryEndpointAddress,
String wsEnumerationConfigurationName,
String wsEnumerationEndpointAddress,
String mexConfigurationName,
String mexEndpointAddress
) : this(
null,
wsTransferConfigurationName,
wsTransferEndpointAddress,
wsTransferFactoryConfigurationName,
wsTransferFactoryEndpointAddress,
wsEnumerationConfigurationName,
wsEnumerationEndpointAddress,
mexConfigurationName,
mexEndpointAddress
)
{
}

public DefaultClient(
NetworkCredential clientCredential,
String wsTransferConfigurationName,
String wsTransferEndpointAddress,
String wsTransferFactoryConfigurationName,
String wsTransferFactoryEndpointAddress,
String wsEnumerationConfigurationName,
String wsEnumerationEndpointAddress,
String mexConfigurationName,
String mexEndpointAddress
)
{
this.wsTransferClient = new WsTransferClient(wsTransferConfigurationName, wsTransferEndpointAddress);
this.wsTransferFactoryClient = new WsTransferFactoryClient(wsTransferFactoryConfigurationName, wsTransferFactoryEndpointAddress);
this.wsEnumerationClient = new WsEnumerationClient(wsEnumerationConfigurationName, wsEnumerationEndpointAddress);
this.mexClient = new MexClient(mexConfigurationName, mexEndpointAddress);

this.resourceFactory = new RmResourceFactory();
this.requestFactory = new RmRequestFactory();

init(clientCredential);
}

private void init(NetworkCredential clientCredential)
{
if (clientCredential != null)
{
ClientCredential = clientCredential;
}
RefreshSchema();
}

Extra Credit

Can anyone tell me why there are warning messages on the following methods in the RmFactory class?
  • IsMultiValued
  • IsReference
  • IsRequired
  • RequiredAttributes

No, really, please tell me; I don't know why they're there. For example:

/// <summary>
/// DO NOT USE THIS METHOD -- FOR TESTING ONLY!
/// </summary>
/// <param name="attributeName"></param>
/// <returns></returns>
public bool IsMultiValued(RmAttributeName attributeName)
{
RmAttributeInfo retValue = null;
RmAttributeCache.TryGetValue(attributeName, out retValue);
if (retValue == null)
{
return false;
}
else
{
return retValue.IsMultiValue;
}
}