Sunday, November 14, 2010

Powershell: get process ID by window name/title

Powershell is cool.

get-process | where {$_.mainwindowtitle -match "pattern"} | format-table id, name, mainwindowtitle -autosize

Friday, November 12, 2010

SSRS data processing extension, Windows integrated security, and impersonation

I've been playing around with Craig Martin's SSRS Data Processing Extension for FIM. (Great stuff; thanks for posting it, Craig!) I had to do some troubleshooting with Windows integrated security and Windows credentials stored securely on the server.

I noticed that the IDbConnection.Open method was being called as the impersonated Windows user:

2010-11-11 14:56:37,932 --4-- DEBUG [FimDataProcessingExtension.FimConnection]    

[Microsoft.ReportingServices.DataProcessing.IDbConnection.Open] Current user info:
Name : TEST\joe.zamora
IsAuthenticated : True
AuthenticationType : Kerberos
ImpersonationLevel : Impersonation

However, the IDbCommand.ExecuteReader method was not run under the context of the Windows user:

2010-11-11 14:56:37,934 --4-- DEBUG [FimDataProcessingExtension.FimConnection]    

[GetData] Current user info:
Name : TEST\svc_ssrs
IsAuthenticated : True
AuthenticationType : Kerberos
ImpersonationLevel : None

Well, turns out that this is by design. Here's the official word from Microsoft:

Impersonation and Custom Data Processing Extensions

If your custom data processing extension connects to data sources using impersonation, you must use the Open method on either the IDbConnection orIDbConnectionExtension interfaces to make the request. Alternately, you can store the user identity object (System.Security.Principal.WindowsIdentity) and then reuse it in the other data processing extension APIs.

In previous releases of Reporting Services, all custom data processing extensions were called under user impersonation. In this release, only the Open method will be called while impersonating the user. If you have an existing data processing extension that requires integrated security, you must modify your code to use the Openmethod or store the user identity object.



Thus, you should save the current Windows identity to a local variable in the IDbConnection.Open method:

if (this.integratedSecurity)
{
this.windowsIdentity = WindowsIdentity.GetCurrent();
}

And then you can use it later in the other API calls:

private WindowsImpersonationContext impersonationContext;
internal void MaybeImpersonate()
{
if (this.integratedSecurity)
{
impersonationContext = this.windowsIdentity.Impersonate();
}
}

That'll teach me for not reading the documenation. ;)

Saturday, August 21, 2010

Data source name not found and no default driver specified

Ahhh, another chance to give back to the community. It's bittersweet. It feels good to contribute, but you really go through the fire to figure out something that no one else has.

This one is an error message that I was getting from SQL Server Integration Services (SSIS). I recently inherited a package (isn't that a nice excuse :) and I switched the connection string from Windows integrated to SQL auth. Pretty standard operation, wouldn't you say? Well, I've been around the block enough to not be totally shocked when I got a few error messages:

An error occured on the SSIS Listener Microsoft.SqlServer.Dts.Runtime.Package/Connection manager "JOESPLACE\MSSQLSERVER.TEST" : SSIS Error Code DTS_E_OLEDBERROR.  An OLE DB error has occurred. Error code: 0x80004005.
An OLE DB record is available. Source: "Microsoft OLE DB Provider for ODBC Drivers" Hresult: 0x80004005 Description: "[Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified".

An error occured on the SSIS Listener Microsoft.SqlServer.Dts.Runtime.TaskHost/Execute SQL Task : Failed to acquire connection "JOESPLACE\MSSQLSERVER.TEMPEST". Connection may not be configured correctly or you may not have the right permissions on this connection.

An error occured while processing the file C:\temp.txt. Details: There was an error executing the SSIS package C:\test.dtsx. Please check the event log for more information.

Okay, so there's an error in the connection string. Let the troubleshooting begin. The thing about this little guy is that he just wouldn't go away. I tried a bunch of different changes to the connection string; nothing worked.

This package is a little different because we're using a variable to specify the table. I suspected that this had something to do with it, so I tried a bunch of different things. It must have been after I switched to an existing table and then back to the table variable that it started working. I didn't know it at the time, because I was being a little sloppy with my trial-and-error.

Anyway, after I got it to work, I compared the dtsx files before and after. Then, starting with the original version, I made one change at a time until it worked. Turns out that the fix was totally a one-liner. Aaarrggghhh! ...and here it is:

<property id="5852" name="OpenRowset" dataType="System.String" state="default" isArray="false" description="Specifies the name of the database object used to open a rowset." typeConverter="" UITypeEditor="" containsID="false" expressionType="None">[dbo].[TEMPEST]</property>


In the original file, the entry was blank; that's it.

A couple more bizarre things about this problem:


  • The package would run in Visual Studio, but not after being published.
  • I deleted the [dbo].[TEMPEST] table and it still worked.


I meant to fix this a while back, but just now got around to it. I think the problem was actually that I was missing the driver in the connection string, just like the error message suggests. Evidently, you can't just use any old .NET connection string; it has to have the driver in it. Ugh.

Thursday, June 10, 2010

Built-in SSRS 2008 Roles

I couldn't find a satisfactory permission matrix for the built-in SSRS 2008 roles, so I'm publishing one here. This is a pretty comprehensive list; you may have to maximize your browser window for this!

For completeness, I'll mention that you can manage these roles by connecting to a Reporting Services instance and browsing the Security folder.

If you connect to the database engine, you can find the permissions encapsulated in the TaskMask column of the Roles table, in the ReportServer database. I would avoid editing the permissions there.

Enjoy!

Built-in RoleSystem AdministratorSystem User
DescriptionView and modify system role assignments, system role definitions, system properties, and shared schedules.View system properties and shared schedules.
TaskMask110101011001010001
TaskDescription
Manage rolesCreate, view, modify and delete role definitions.X
Manage report server securityView and modify system-wide role assignments.X
View report server propertiesView properties that apply to the report server.X
Manage report server propertiesView and modify properties that apply to the report server and to items managed by the report server.X
View shared schedulesView a predefined schedule that has been made available for general use.X
Manage shared schedulesCreate, view, modify and delete shared schedules used to run reports or refresh a report.X
Generate eventsProvides an application with the ability to generate events within the report server namespace.
Manage jobsView and cancel running jobs.X
Execute Report DefinitionsStart execution from report definition without publishing it to Report Server.XX



Built-in RoleBrowserContent ManagerMy ReportsPublisherReport Builder
DescriptionMay view folders, reports and subscribe to reports.May manage content in the Report Server. This includes folders, reports and resources.May publish reports and linked reports; manage folders, reports and resources in a users My Reports folder.May publish reports and linked reports to the Report Server.May view report definitions.
TaskMask00101010010001001111111111111111011111111101100001010101000010100010101001000101
TaskDescription
Set security for individual itemsView and modify security settings for reports, folders, resources, and shared data sources.X
Create linked reportsCreate linked reports and publish them to a report server folder.XXX
View reportsView reports and linked reports in the folder hierarchy; view report history snapshots and report properties.XXXX
Manage reportsCreate, and delete reports; and modify report properties.XXX
View resourcesView resources in the folder hierarchy; and view resource properties.XXXX
Manage resourcesCreate, modify and delete resources, and modify resource properties.XXX
View foldersView folder items in the folder hierarchy; and view folder properties.XXXX
Manage foldersCreate, view and delete folders; and view and modify folder properties.XXX
Manage report historyCreate, view, and delete report history snapshots; and modify report history properties.XX
Manage individual subscriptionsEach user can create, view, modify and delete subscriptions that he or she owns.XXXX
Manage all subscriptionsView, modify, and delete any subscription regardless of who owns the subscription.X
View data sourcesView shared data source items in the folder hierarchy; and view data source properties.XX
Manage data sourcesCreate and delete shared data source items; and modify data source properties.XXX
View modelsView models in the folder hierarchy, use models as data sources for a report, and run queries against the model to retrieve data.XXX
Manage modelsCreate, view, and delete models; and view and modify model properties.XX
Consume reportsReads report definitionsXX

Tuesday, June 8, 2010

SQL Server 2008 Reporting Services RS.EXE Supporting Forms Authentication

I've decided to expand the scope of my blog to more than just Identity Management. Recently, I've been submerged in the BI space, and since I'm not sure when I'll resurface, I'd like to contribute some of the cool stuff I've been doing back to the community.

I'm currently working on a scale-out deployment of SSRS that uses Forms authentication. Everything is working well with the security extension, and now we're focusing on moving everything into Stage. Good time for the scripting utility, rs.exe, right? Well, if you didn't already know, rs.exe isn't supported with Forms authentication. Wow, what a bummer.

What's more, I can't use the RSScripter, which is a very cool and useful app for generating scripts for rs.exe.

So, what do I do? I could write my own utility. We already have a web service client to integrate SSRS with an in-house app. I would just need to add a bunch of management features to it... and add commands to extract and load... and make it generic enough to reuse... and then maintain it. Nah, that sounds like a lot of work. Let's just use .NET Reflector on the original rs.exe app!

I've reassembled rs.exe with the ability to preserve the RS authentication cookie, which is required with Forms authentication. Hopefully someone out there finds this useful!

http://rs1.codeplex.com/

BTW, the RSScripter also doesn't support Forms authentication, because rs.exe doesn't, and RSScripter is dependent on rs.exe. Leave me a comment if you need a version of RSScripter that works with Forms auth.

Good luck!

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

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;
}
}