Tips on deploying TFS in a multi-domain environment

We have been asked about issues related to TFS in a multi-domain environment sporadically. I’m writing this post to share some basic knowledge in this scenario and wish you can benefit from it. Before you continue, please take a look at Trusts and Forests Considerations for Team Foundation Server.

Tip 1: The domain that the TFSSERVICE account is in needs to be trusted by user domains.

TFS caches user accounts in its own databases. In certain conditions, TFS synchronizes cached accounts with AD. See my previous post TFS service account requires read permission in user domains for more information. For instance, you have a domain group Developers in AD. The Developers group is added to the Contributors group of a team project. You then added a new employee Jim to the Developers group. TFS considers Jim is in Contributors group only after a synchronization.

TFS uses the TFSSERVICE account to synchronize user accounts. If your users accounts are from multiple domains, then the TFSSERVICE account needs to be authenticated by all user domains. 

Tip2: When you add users of another domain to a TFS group, you need to log in to your computer with an account that is trusted by the other domain.

Suppose you log in to your computer with DomainA\user1, and you plan to add DomainB\user2 to the Contributors group of a team project with Team Explorer. Then DomainA needs to be trusted by DomainB. Otherwise, you will not be able to specify DomainB\user2 in the Select Users dialogbox like the below:

SelectUser

Remove old shelvesets

Today a customer asked me how to clean up old shelvesets.The October 2008 release of TFS Power Tool contains Get-TfsShelveset and Remove-TfsShelveset cmdlets. The issue is that if one passes the result of Get-TfsShelveset to Remove-TfsShelveset with pipeline, the owner information is ignored and the Remove-TfsShelveset cmdlet will attempt to look up shelveset created by the current user.

Fortunately, Remove-TfsShelveset recognizes owner in terms of “Remove-TfsShelveset –Shelveset name;domain\alias”. So one workaround is to first obtain a list of shelvesets with Get-TfsShelveset cmdlet, then computer the shelvesetspecs and invoke Remove-TfsShelveset understands. The below script will delete all shelvesets created before 90 days ago.

Get-TfsShelveset -server http://170112m3:8080 -owner * | 
Where-Object -Filter {$_.CreationDate -lt [DateTime]::Now.AddDays(-90)} |
ForEach-Object -Process { Remove-TfsShelveset -server http://170112m3:8080 -Shelveset
($_.Name+";"+$_.OwnerName) }

VS Macro for deleting line numbers

When you copy code from internet and paste into Visual Studio, have you ever faced the trivial task to remove line numbers in the front of each line of code? I just wrote a macro to do this and I’d like to share it with you.

When we past code into Visual Studio, it often looks like this:

MacroForRemovingLinenumber

With this macro, we can remove line numbers very quickly. And the following is the script code. It will set the cursor to the start of current opened document, parse the first 3 characters of each line until the end of the document.

    Sub DeleteLineNumber()
Dim objSel As TextSelection = DTE.ActiveDocument.Selection

objSel.EndOfDocument()
Dim endLine As Integer = objSel.ActivePoint.Line
objSel.StartOfDocument()

For line = 1 To endLine

For bit = 1 To 3
objSel.CharRight(True)
Dim number As Integer
If Integer.TryParse(objSel.Text, number) Then
objSel.DeleteLeft()
End If
Next

objSel.LineDown()
objSel.StartOfLine()
Next

End Sub


To use this macro, you can




  1. Launch Visual Studio


  2. Press ALT+F8 to open Macro Explorer


  3. Expand MyMacros node in Macro Explorer


  4. Right click a module and select ‘New macro’ to create a new macro.


  5. Past in the script.



And there are several ways to run a macro. They are described in How to: Run Macros. If you find any issue or has any comment, please feel free to let me know

Install_Msxml6_Cpu32_Action failed when installing SQL Server 2008

hfare One of my friends was trying to setup a TFS 2010 environment, which requires SQL Server 2008 or above versions. We came across a SQL Server 2008 RTM bug, so I’m posting its description here so that other folks can work around it too.

Symptoms

The installation wizard of SQL Server 2008 fails at the point of starting the SQL Server browser service. In the detailed installation log of SQL Server, which locates in %ProgramFiles%\Microsoft SQL Server\100\Setup Bootstrap\LOG\, there are error messages like the following. See How to: View SQL Server Setup Log Files for more information about log location.

2009-07-01 16:19:19 Slp: Checkpoint: INSTALL_MSXML6_CPU32_ACTION
2009-07-01 16:19:19 Slp: Completed Action: Install_Msxml6_Cpu32_Action, returned False
2009-07-01 16:19:19 Slp: Error: Action "Install_Msxml6_Cpu32_Action" failed during execution.
2009-07-01 16:19:19 Slp: Action "Install_sql_engine_core_shared_Cpu32_Action" will return false due to the following conditions:

Cause

This is a SQL Server 2008 RTM installation bug. The installation wizard has a conflict with MSXML6 SP2 (KB954459).

Workaround

  1. Remove KB954459
  2. Uninstall SQL Server 2008.
  3. Reboot  the computer.
  4. Install SQL Server 2008.

Moving Reporting Service in a TFS 2008 environment

On MSDN, we have documents for moving TFS database, moving Analysis Service database etc. But it’s not covered to move Reporting Service.

After I sent this question to our internal discussion group. I received a couple of replies. Especially thank Aaron Block, Bill Essary and Lakhminder Singh for their great help. Here are the steps to move RS.

  1. Follow KB 842425 to move RS DB to a new instance.
  2. Ensure that you have a login for the TFS Reports account, in case your RS databases are now going to be hosted on a different SQL instance. The login for the RS database should have RSExecRole in Report Server databases (which it should retain when you restore the RS databases.)
  3. Create new data connections for TfsReportsDS and TfsOlapReportsDS. They can be modified via http://ReportingService/Reports.
  4. Run TfsAdminUtil ConfigureConnections command to update setting /ReportsUri and /ReportServerUri.  Consider http://support.microsoft.com/kb/959126 while doing it.
  5. Copy TfsConfigWss.exe to the SharePoint server and run it there to update the reports redirector.
  6. In the installation folder of TFS, there’s a folder named “Microsoft Visual Studio 2008 Team Foundation Server - ENU”. Update the MSIProperty.ini in that folder and change the RS machine name for the VSTF_RS_SERVER property. You just need to give new machine\server name for RS & do not give the instance name. Optionally update the VSTF_RS_REPORTS_URI, & VSTF_RS_REPORTSERVER_URI properties. See Msiproperty.ini File Properties for more information.

Testing WCF web services

This post is for refining my previous post Test WCF web service – Hosted in ASP.NET Development Server. Personally, I prefer Wordpress because it is easy to maintain layout. But all blogs at wordpress.com are censored by the Great Firewall of China (I always want to say the word that has 4 characters, starts with “F” and ends with “K” when talking about it). So I decided to continue to blog in BlogSpot.

I started working in Microsoft Shanghai Global Technical Support Center on May 5th 2008. Fortunately, the company has a private line connecting to the USA thus we can bypass the Great Firewall in the office. My friends and I have dreamed this all our lives. When I wrote the previous post about testing WCF web service, I just had several weeks of experience in VSTT, so forgive me for not having made it easy to read. Since published that post, I have received many feedbacks, thank you all! It’s time to refine the post.

I remember last year the MSDN document at http://msdn.microsoft.com/en-us/library/ms243399.aspx was the same as that in http://msdn.microsoft.com/en-us/library/ms243399(VS.80).aspx. Then Microsoft has changed the terminology about unit test. It is said that hosting a web service and actually invoking it over HTTP in a test method is considered to be “integration test”, while creating an instance of the web service class in a test method and invoking it’s methods is the “unit test”. So I guess the topic of this post is about how to host WCF web services and perform integration testing.

I focus on testing WCF web services that are hosted in ASP.NET Deployment Server (Cassini).

Background about ASP.NET unit testing.

Let me explain some background about ASP.NET unit testing. ASP.NET unit tests can (not always) be executed in the same process as the web server. See Overview of ASP.NET Unit Tests for more information. At run time of executing ASP.NET unit tests, it goes through the following steps:

  1. Backup your web.config to web.config.backup.
  2. Register an HTTP module Microsoft.VisualStudio.TestTools.HostAdapter.Web.HttpModule, which is in %Program Files%\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\Microsoft.VisualStudio.QualityTools.HostAdapters.ASPNETAdapter. to the web.config.
  3. Start ASP.NET Deployment Server if it is used.
  4. Send a request to the URL pointed by UrlToTest attribute of your test method.
  5. The registered Microsoft.VisualStudio.TestTools.HostAdapter.Web.HttpModule is triggered by the web server. The module subscribes the PageLoad event of the requested page. When PageLoad event is fired, it loads the test assembly in to the web server process and execute the test method.
  6. Restore the web.config and delete web.config.backup after tests complete.

ASP.NET Deployment Server is started when running a unit test if one of the following 2 conditions is meet:

  1. AspNetDeploymentServer attribute is specified.
    This attributes tells VS to start Cassini before executing the test method. Both AspNetDeploymentServer and AspNetDeploymentServerHost expects a physical path to the web application. In a team environment, the web site is usually located in different directories in different computers. The workaround is to use environment variables. See Testing Web Sites and Web Services in a Team Environment for more information. 
  2. UrlToTest and HostType and  AspNetDeploymentServerHost attributes are all specified.
    UrlToTest attribute is required if HostType attribute is set to “ASP.NET”. The combination of the 3 attributes let VS know ASP.NET Deployment Server should be started and the test method should be executed in the same process as the web server.

When testing web services, we usually want to invoke the web services from a proxy and it’s not necessary to execute test methods in the same process as the web server. Therefore, we don’t need to UrlToTest, HostType and AspNetDeploymentServerHost attributes.

Testing WCF web services hosted in ASP.NET Deployment Server

I go through the process of creating a WCF project and add a test method to demonstrate this scenario. Several sentences here are copied from http://msdn.microsoft.com/en-us/library/ms243399(VS.80).aspx.

  1. Create a WCF project in VS via File->New->Project->Visual C#->Web->WCF Service Application.
  2. Generate unit tests against the Web service in the standard way for generating unit tests. For more information, see How to: Generate a Unit Test.
  3. Use svcutil to generate WCF web service proxy.
    1. Right click the Service1.svc in the WCF project in Solution Explorer and select View in Browser.
    2. Run svcutil http://localhost:52747/Service1.svc /config:app.config /out:Service1Proxy.cs /language:C# to generate proxy and config file. Please replace the URL of Service1.svc to that was displayed in the address bar of the browser in #1.
    3. Add Service1Proxy.cs and app.config to the test project.
  4. Remove the attribute HostType, UrlToTest and AspNetDeploymentServerHost for the generated test method.
  5. Add the AspNetDevelopmentServerAttribute attribute to the unit test. The arguments for this attribute class point to the site of the Web service and name the server. For more information, see Ensuring Access to ASP.NET Development Server.
  6. Change the test method to use the generated proxy class to invoke the web service and add the redirection logic. The test class will look like the following.
DownloadIcon sample project
I also paste some code here to ease reverences.

WcfWebServiceHelper.cs

using System;
using System.Reflection;
using System.ServiceModel.Description;
using System.ServiceModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestProject1
{
class WcfWebServiceHelper
{
public static bool TryUrlRedirection(object client, TestContext context, string identifier)
{
bool result = true;
try
{
PropertyInfo property = client.GetType().GetProperty("Endpoint");
string webServer = context.Properties[
string.Format("AspNetDevelopmentServer.{0}", identifier)].ToString();
Uri webServerUri = new Uri(webServer);
ServiceEndpoint endpoint = (ServiceEndpoint)property.GetValue(client, null);

EndpointAddressBuilder builder = new EndpointAddressBuilder(endpoint.Address);
builder.Uri = new Uri(
endpoint.Address.Uri.OriginalString.Replace(
endpoint.Address.Uri.Authority, webServerUri.Authority));

endpoint.Address = builder.ToEndpointAddress();

}
catch (Exception e)
{
context.WriteLine(e.Message);
result = false;
}
return result;
}

}
}


Test Method



        [TestMethod()]
[AspNetDevelopmentServer("WcfService1",
"C:\\Users\\Administrator\\Desktop\\TfsRoot\\BuildTest\\Main\\MyServices\\WcfService1")]
public void GetDataTest()
{
Service1Client target = new Service1Client();
Assert.IsTrue(WcfWebServiceHelper.TryUrlRedirection(target,TestContext,"WcfService1"));
int value = 0;
string expected = "You entered: 0";
string actual;
actual = target.GetData(value);
Assert.AreEqual(expected, actual);
}

How to: Initialize a WorkItem instance for the changed work item in WorkItemChangedEvent handler

Recently I’ve been asked about several questions that need to initialize a WorkItem instance in the web service handler for WorkItemChangedEvent. So I paste a sample code here to ease the future references.

One thing to note is that in order to query work items, the identity for the app pool that hosts the web service in IIS must has property permission granted. In TFS 2005 and 2008, work item permissions are based on area and iteration. See Team Foundation Server Permissions for more information.

using System;
using System.Diagnostics;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

namespace EventHandler
{
/// <summary>
/// Summary description for Service1
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
// [System.Web.Script.Services.ScriptService]
public class WorkItemChangedEventHandler : System.Web.Services.WebService
{

[SoapDocumentMethod(
Action = "http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify",
RequestNamespace = "http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03")]
[WebMethod]
public void Notify(string eventXml)
{
try
{
string tfsUrl = "http://TFS_AT:8080";
TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(tfsUrl);

var workItemId = GetWorkItemID(eventXml);

var wis = (WorkItemStore)tfs.GetService(typeof(WorkItemStore));
var wi = wis.GetWorkItem(workItemId);


}
catch (Exception e)
{
var eventSource = "My WorkItemChangedEvent Handler";
var logName = "Application";
var machineName = "."; //local computer.

if (!EventLog.SourceExists(eventSource, machineName))
{
var eventObj = new EventSourceCreationData(eventSource, logName);
EventLog.CreateEventSource(eventObj);
}

var eventLog = new EventLog(logName, machineName, eventSource);
eventLog.WriteEntry(e.Message);
}
}

private int GetWorkItemID(string eventXml)
{
var doc = new XmlDocument();
doc.LoadXml(eventXml);
var path = "/WorkItemChangedEvent/CoreFields/IntegerFields/Field[ReferenceName='System.Id']/NewValue";
var node = doc.SelectSingleNode(path);

var id = int.Parse(node.InnerText);
return id;
}

}
}