How can you find unused NuGet packages in solution? - visual-studio

How can you find the unused NuGet packages in a solution?
I've got a number of solutions where there are a lot of installed packages, and a large number of them are flagged as having updates.
However, I'm concerned there may be breaking changes, so I first want to clean up by removing any unused packages.

ReSharper 2016.1 has a feature to remove unused NuGet.
It can be run on a solution and on each project in a solution and it does the following things:
Analyze your code and collecting references to assemblies.
Build NuGet usage graph based on usages of assemblies.
Packages without content files, unused itself and without used dependencies are assumed as unused and suggested to remove.
Unfortunately, this doesn't work for project.json projects (RSRP-454515) and ASP.NET core projects (RSRP-459076)

Visual Studio 2019 (version 16.9) has the remove-unused-packages function built-in, we will need to enable it manually now.
Go to Tools > Options > Text Editor > C# > Advanced > (Under the Analysis section) Tick Show "Removed Unused References" command
Visual Studio version 16.10 has the remove unused reference feature available. Right-click on the project > Remove Unused References.

You can use the Visual Studio Extension ResolveUR - Resolve Unused References.
Resolve unused references including nuget references in Visual Studio 2012/2013/2015 projects via menu item at solution and project nodes Solution Explorer Tool Window
It's not an easy task, so I suggest to make a backup and/or commit before, just in order to rollback if something went wrong.

You can accomplish this using ReSharper 2019.1.1.
Right click on the project > Refactor > Remove Unused References.
If your project is small, you can also use: project > Optimize Used References . . .
A window will pop up. Select all references and remove them all. Then go back and re-add the ones that give you a compiler error.

Right-click on the Dotnet core project in visual studio 2019 you will see an option for Remove unused references.

Below is a little PowerShell script that finds redundant NuGet packages for .NET Core / .NET 5+ projects. For each project file it removes every reference once and checks if it compiles. This will take a lot of time. After this you get a summary of each reference that might be excluded. In the end it is up to you do decide what should be removed. Most likely you will not be able to remove everything it suggest (due dependencies), but it should give you a good starting point.
Save the script below as a ps1-file and replace the string C:\MySolutionDirectory in line 89 with the directory you want to scan on and then run the ps1-file. Do an backup first in case something goes wrong.
function Get-PackageReferences {
param($FileName, $IncludeReferences, $IncludeChildReferences)
$xml = [xml] (Get-Content $FileName)
$references = #()
if($IncludeReferences) {
$packageReferences = $xml | Select-Xml -XPath "Project/ItemGroup/PackageReference"
foreach($node in $packageReferences)
{
if($node.Node.Include)
{
if($node.Node.Version)
{
$references += [PSCustomObject]#{
File = (Split-Path $FileName -Leaf);
Name = $node.Node.Include;
Version = $node.Node.Version;
}
}
}
}
}
if($IncludeChildReferences)
{
$projectReferences = $xml | Select-Xml -XPath "Project/ItemGroup/ProjectReference"
foreach($node in $projectReferences)
{
if($node.Node.Include)
{
$childPath = Join-Path -Path (Split-Path $FileName -Parent) -ChildPath $node.Node.Include
$childPackageReferences = Get-PackageReferences $childPath $true $true
$references += $childPackageReferences
}
}
}
return $references
}
function Get-ProjectReferences {
param($FileName, $IncludeReferences, $IncludeChildReferences)
$xml = [xml] (Get-Content $FileName)
$references = #()
if($IncludeReferences) {
$projectReferences = $xml | Select-Xml -XPath "Project/ItemGroup/ProjectReference"
foreach($node in $projectReferences)
{
if($node.Node.Include)
{
$references += [PSCustomObject]#{
File = (Split-Path $FileName -Leaf);
Name = $node.Node.Include;
}
}
}
}
if($IncludeChildReferences)
{
$projectReferences = $xml | Select-Xml -XPath "Project/ItemGroup/ProjectReference"
foreach($node in $projectReferences)
{
if($node.Node.Include)
{
$childPath = Join-Path -Path (Split-Path $FileName -Parent) -ChildPath $node.Node.Include
$childProjectReferences = Get-ProjectReferences $childPath $true $true
$references += $childProjectReferences
}
}
}
return $references
}
$files = Get-ChildItem -Path C:\MySolutionDirectory -Filter *.csproj -Recurse
Write-Output "Number of projects: $($files.Length)"
$stopWatch = [System.Diagnostics.Stopwatch]::startNew()
$obseletes = #()
foreach($file in $files) {
Write-Output ""
Write-Output "Testing project: $($file.Name)"
$rawFileContent = [System.IO.File]::ReadAllBytes($file.FullName)
$childPackageReferences = Get-PackageReferences $file.FullName $false $true
$childProjectReferences = Get-ProjectReferences $file.FullName $false $true
$xml = [xml] (Get-Content $file.FullName)
$packageReferences = $xml | Select-Xml -XPath "Project/ItemGroup/PackageReference"
$projectReferences = $xml | Select-Xml -XPath "Project/ItemGroup/ProjectReference"
$nodes = #($packageReferences) + #($projectReferences)
foreach($node in $nodes)
{
$previousNode = $node.Node.PreviousSibling
$parentNode = $node.Node.ParentNode
$parentNode.RemoveChild($node.Node) > $null
if($node.Node.Include)
{
$xml.Save($file.FullName)
if($node.Node.Version)
{
$existingChildInclude = $childPackageReferences | Where-Object { $_.Name -eq $node.Node.Include -and $_.Version -eq $node.Node.Version } | Select-Object -First 1
if($existingChildInclude)
{
Write-Output "$($file.Name) references package $($node.Node.Include) ($($node.Node.Version)) that is also referenced in child project $($existingChildInclude.File)."
continue
}
else
{
Write-Host -NoNewline "Building $($file.Name) without package $($node.Node.Include) ($($node.Node.Version))... "
}
}
else
{
$existingChildInclude = $childProjectReferences | Where-Object { $_.Name -eq $node.Node.Include } | Select-Object -First 1
if($existingChildInclude)
{
Write-Output "$($file.Name) references project $($node.Node.Include) that is also referenced in child project $($existingChildInclude.File)."
continue
}
else
{
Write-Host -NoNewline "Building $($file.Name) without project $($node.Node.Include)... "
}
}
}
else
{
continue
}
dotnet build $file.FullName > $null
if($LastExitCode -eq 0)
{
Write-Output "Building succeeded."
if($node.Node.Version)
{
$obseletes += [PSCustomObject]#{
File = $file;
Type = 'Package';
Name = $node.Node.Include;
Version = $node.Node.Version;
}
}
else
{
$obseletes += [PSCustomObject]#{
File = $file;
Type = 'Project';
Name = $node.Node.Include;
}
}
}
else
{
Write-Output "Building failed."
}
if($null -eq $previousNode)
{
$parentNode.PrependChild($node.Node) > $null
}
else
{
$parentNode.InsertAfter($node.Node, $previousNode.Node) > $null
}
# $xml.OuterXml
$xml.Save($file.FullName)
}
[System.IO.File]::WriteAllBytes($file.FullName, $rawFileContent)
dotnet build $file.FullName > $null
if($LastExitCode -ne 0)
{
Write-Error "Failed to build $($file.FullName) after project file restore. Was project broken before?"
return
}
}
Write-Output ""
Write-Output "-------------------------------------------------------------------------"
Write-Output "Analyse completed in $($stopWatch.Elapsed.TotalSeconds) seconds"
Write-Output "$($obseletes.Length) reference(s) could potentially be removed."
$previousFile = $null
foreach($obselete in $obseletes)
{
if($previousFile -ne $obselete.File)
{
Write-Output ""
Write-Output "Project: $($obselete.File.Name)"
}
if($obselete.Type -eq 'Package')
{
Write-Output "Package reference: $($obselete.Name) ($($obselete.Version))"
}
else
{
Write-Output "Project refence: $($obselete.Name)"
}
$previousFile = $obselete.File
}
You find more information here: https://devblog.pekspro.com/posts/finding-redundant-project-references

In Visual Studio 2019 starting from the latest versions and Visual Studio 2022 you can remove unused packages as reported in previous comments, but only for SDK style projects.
If you try on old projects, like .Net Framework, you won't see this option.
As workaround, to verify, you can create two simply console apps: one using .Net Core or later, and one .Net Framework 4.7 or 4.8.
Please refer to: Remove Unused References

This is manual labor, but it works.
Use ReSharper or similar code analysis tool to identify any unused references in your projects and uninstall the nuget in the corresponding projects.
Sometimes uninstalled nugets still linger in the Installed packages and Updates lists in the Manage NuGet Packages dialog. Close Visual Studio then delete the packages folder, then reopen the solution and restore your nugets.

I don't think there is a default way to find this out. The primary reason being the variety of things these packages can do from referencing an assembly to injecting source code to your project. You may want to check the Nuget.Extensions though. The following thread on codeplex talks about an audit report of nuget packages.
http://nuget.codeplex.com/discussions/429694
(NuGet has been moved from Codeplex to GitHub. Archive of the above link:)
https://web.archive.org/web/20171212202557/http://nuget.codeplex.com:80/discussions/429694

In JetBrains Rider IDE
Right-click solution or project
"Refactor This"
"Remove Unused References"
Next...
https://www.jetbrains.com/help/rider/Refactorings__Remove_Unused_References.html

Related

download NuGet Licenses from VS Project and export it as plaintext

is there a way to convert html to plaintext?
I have a script that exports all NuGet-Licenses which been used in a visual studio project to a textfile.
Unfortunately the exports are mostly in HTML, and I found no way to solve it.
# Run in Package Manager Console with `./download-packages-license.ps1`.
# If access denied, execute `Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned`.
# Save licenses to One text file and one csv file instead of individual files
$LicensesFile = (Join-Path (pwd) 'licenses\Licenses.txt')
$LicensesFile_csv = (Join-Path (pwd) 'licenses\Licenses.csv')
$results = #()
# Below 2 lines to comment if you uncomment Split-Path ..
$solutionFile = "d:\Solutions\SolFile.sln"
cd "d:\Solutions"
# Uncomment below line if you wish to want to use above 2 lines
# Split-Path -parent $dte.Solution.FileName | cd;
New-Item -ItemType Directory -Force -Path ".\licenses";
#( Get-Project -All | ? { $_.ProjectName } | % {
Get-Package -ProjectName $_.ProjectName | ? { $_.LicenseUrl }
} ) | Sort-Object Id -Unique | % {
$pkg = $_;
Try
{
if ($pkg.Id -notlike 'microsoft*' -and $pkg.LicenseUrl.StartsWith('http'))
{
Write-Host ("Download license for package " + $pkg.Id + " from " + $pkg.LicenseUrl);
#Write-Host (ConvertTo-Json ($pkg));
$licenseUrl = $pkg.LicenseUrl
if ($licenseUrl.contains('github.com')) {
$licenseUrl = $licenseUrl.replace("/blob/", "/raw/")
}
$extension = ".txt"
if ($licenseUrl.EndsWith(".md"))
{
$extension = ".md"
}
(New-Object System.Net.WebClient).DownloadFile($licenseUrl, (Join-Path (pwd) 'licenses\') + $pkg.Id + $extension);
$licenseText = get-content "$((Join-Path (pwd) 'licenses\') + $pkg.Id + $extension)"
Remove-Item $((Join-Path (pwd) 'licenses\') + $pkg.Id + $extension) -ErrorAction SilentlyContinue -Force
$data = '' | select PkgId, LicenseText
$data.PkgId = $pkg.Id
$data.LicenseText = $licenseText | Out-String
$results += $data
# save in txt file
"Designation: NugetPackage $($pkg.Id)" | Add-Content $LicensesFile
$licenseText | Add-Content $LicensesFile
"" | Add-Content $LicensesFile
"" | Add-Content $LicensesFile
"" | Add-Content $LicensesFile
"" | Add-Content $LicensesFile
Write-Host "Package $($pkg.Id): License Text saved to $LicensesFile" -ForegroundColor Green
}
}
Catch [system.exception]
{
Write-Host ("Could not download license for " + $pkg.Id)
}
}
# save in .csv file
$results | Export-Csv $LicensesFile_csv -nti
Source of the Script here
A user also said ,,Unfortunately, most license URLs now point to HTML-only versions (early 2020). For example, licenses.nuget.org ignores any "Accept: text/plain" (or json) headers and returns html regardless"
So is there even a way to get the license information in plaintext?
Thanks and stay healthy!
So is there even a way to get the license information in plaintext?
Actually, we do not recommend that you convert the html file into plaintext format. And when you get the license data from nuget.org, it is the data returned from the site in full HTML format, which is designed by that.
The returned data also contains various formats for the license field, so we should not easily modify the accepted data format(such as plaintext ). And if possible, the only way to do this is to get rid of the HTML format fields from the source data, but however, it is impossible by Powershell and it cannot be done so far.
Therefore, in order to strictly follow the format of the returned data, it is best to use an HTML file to receive license info. It can maintain consistency with the website in the form of html.
Suggestion
1) change these in powershell:
$LicensesFile = (Join-Path (pwd) 'licenses\Licenses.html')
$LicensesFile_csv = (Join-Path (pwd) 'licenses\Licenses_csv.html')
And then you can get what you want.
Hope it could help you.

How to change filtering in Powershell script to create dynamic start menu with .url and .website?

Update: I am now attempting to use a Get-StartApps command instead of recursively going through the files with a filter. Now I am in need of help re-writing the logic behind the insert sections noted in #3 below. Any suggestions would be amazing!
I found and have been working with a series of scripts that can be used to create a start menu XML. The version I have settled on is below. Unfortunately, I cannot seem to get it to handle ".URL",".Website", ".shortcut" extensions.
I have tried the following (one at a time and together):
On line 90 removing the -filter "*.lnk"
Adding -filter ".website", -Filter ".shortcut"... to line 90
Creating ifelse statements for each of these extensions modeled after lines 107-116
if ($SoftwareLinks.Name -like "$Software.lnk") {
$SoftwareLink = $SoftwareLinks | where {$_ -like "$Software.lnk"}
$child = $StartMenuXml.CreateElement("start","DesktopApplicationTile","http://schemas.microsoft.com/Start/2014/StartLayout")
if ($SoftwareLink.FullName.GetType().BaseType -eq [System.Array]) { ## If multiple links, use the first one
$child.SetAttribute('DesktopApplicationLinkPath',$SoftwareLink.FullName[0])
} else {
$child.SetAttribute('DesktopApplicationLinkPath',$SoftwareLink.FullName)
}
}
None of this seems to work when trying to pick up links that are placed in the start menu folder. Has anyone come across this? Do you have any advice on how to fix this? Whole script below:
# Where to save start menu xml
#$StartMenuFile = "$ENV:programfiles\DESKTOPENGINEERING\startmenu\startmenu.xml"
$StartMenuFile = "$ENV:programfiles\DESKTOPENGINEERING\startmenu\startmenu.xml"
#$OldStartMenuFile = "$ENV:programfiles\DESKTOPENGINEERING\startmenu\startmenu.xml"
$OldStartMenuFile = "$programfiles\DESKTOPENGINEERING\startmenu\startmenu.old"
# Set this to SilentlyContinue for no debug, or Continue for debug output
$DebugPreference = "SilentlyContinue"
# Remove old startmenu.old file
IF (Test-path $OldStartMenuFile) {
Write-Debug "The file `"$OldStartMenuFile`" already exists and will be removed!"
Remove-item $OldStartMenuFile -Force
} Else {
Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."
}
# Rename startmenu.xml to startmenu.old
IF (Test-path $StartMenuFile) {
Write-Debug "renaming file `"$OldStartMenuFile`"..."
Rename-Item -Path $StartMenuFile -NewName $OldStartMenuFile -Force
} Else {
Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."
}
# One last check to see if file exists or not
IF (Test-path $StartMenuFile) {
Write-Error "Could not rename `"$OldStartMenuFile`", script aborted"
Break
} Else {
Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."
}
# Make sure folder exist and halt if it can't be created
$StartMenuFolder=(Split-path -parent $StartMenuFile)
IF (Test-path $StartMenuFolder) { } ELSE { New-Item -ItemType Directory -Path $StartMenuFolder }
IF (Test-path $StartMenuFolder) { } ELSE { Write-Error "Could not create `"$StartMenuFolder`", script aborted" ; Break }
# Specify number of cols in startmenu
$NumCol = 6
# Add the new group in $MenuGroups
# Format: "order. group title" = "list of Software Links", "Followed by other links"
$MenuGroups = #{
"1. Internet & Network tools" = "Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge"
"2. Microsoft Office" = "Access 2016","Excel 2016","Outlook 2016","PowerPoint 2016","Project 2016","Publisher 2016","Word 2016"
"3. Text, file & programming tools" = "Calculator","Notepad"
"4. Media tools" = "Paint"
"5. Scientific software" = "Calculator"
"6. Administrator" = "Powershell"
"7. Other tools" = "Google Chrome", "Google Link"
}
# Building up base startmenu xml
[xml]$StartMenuXml = '<LayoutModificationTemplate
xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout"
xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout"
xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout"
Version="1">
<LayoutOptions StartTileGroupsColumnCount="1" StartTileGroupCellWidth="'+$NumCol+'" />
<DefaultLayoutOverride LayoutCustomizationRestrictionType="OnlySpecifiedGroups">
<StartLayoutCollection>
<defaultlayout:StartLayout GroupCellWidth="'+$NumCol+'" xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout">
</defaultlayout:StartLayout>
</StartLayoutCollection>
</DefaultLayoutOverride>
<CustomTaskbarLayoutCollection PinListPlacement="Replace">
<defaultlayout:TaskbarLayout>
<taskbar:TaskbarPinList>
<taskbar:UWA AppUserModelID="Microsoft.WindowsCalculator_8wekyb3d8bbwe!App"/>
</taskbar:TaskbarPinList>
</defaultlayout:TaskbarLayout>
</CustomTaskbarLayoutCollection>
</LayoutModificationTemplate>'
# Selecting XML element where all software will be placed
$DefaultLayoutElement = $StartMenuXml.GetElementsByTagName("defaultlayout:StartLayout")
# Fetching all software links on start menu
$SoftwareLinks = Get-ChildItem "$env:PROGRAMDATA\Microsoft\Windows\Start Menu" -recurse -filter "*.lnk"
# Looping all menu groups defined above
foreach ($MenuGroup in $MenuGroups.Keys | Sort-Object) {
# Init xml element for software group
$SoftwareGroupXml = $StartMenuXml.CreateElement("start","Group", "http://schemas.microsoft.com/Start/2014/StartLayout")
$SoftwareGroupXml.SetAttribute('Name',$MenuGroup.Substring(3))
# Init row and col
$col = 0
$row = 0
# Looping all software links in start menu
foreach ($Software in $MenuGroups[$MenuGroup]) {
# Check if it is time for a new col
if (($col%($NumCol-1) -eq 1) -and ($col -ne 1)) {
$row +=1
$col = 0
}
# Check if specified software is found in start menu. If so, add software element
if ($SoftwareLinks.Name -like "$Software.lnk") {
$SoftwareLink = $SoftwareLinks | where {$_ -like "$Software.lnk"}
$child = $StartMenuXml.CreateElement("start","DesktopApplicationTile","http://schemas.microsoft.com/Start/2014/StartLayout")
if ($SoftwareLink.FullName.GetType().BaseType -eq [System.Array]) { ## If multiple links, use the first one
$child.SetAttribute('DesktopApplicationLinkPath',$SoftwareLink.FullName[0])
} else {
$child.SetAttribute('DesktopApplicationLinkPath',$SoftwareLink.FullName)
}
}
# Or check if Microsoft app is specified. If so add app element
elseif ($Software -like "Microsoft.*!*") {
$child = $StartMenuXml.CreateElement("start","Tile","http://schemas.microsoft.com/Start/2014/StartLayout")
$child.SetAttribute('AppUserModelID',$Software)
}
# Add common attributes is software or app is found and append xml element
if (($child.HasAttributes) -and (($Software -like "Microsoft.*!*") -or ($SoftwareLinks.Name -like "$Software.lnk"))) {
$child.SetAttribute('Size','2x2')
$child.SetAttribute('Column',$col)
$child.SetAttribute('Row',$row)
$SoftwareGroupXml.AppendChild($child) | Out-Null
$col +=1
}
}
# If a software group is not null, add it!
if ($SoftwareGroupXml.HasChildNodes) {
$DefaultLayoutElement.AppendChild($SoftwareGroupXml) | Out-Null
}
}
# Save to file
$StartMenuXml.Save($StartMenuFile)### Script ends ###
Ok so there was a two part fix. Instead of recursively searching the folders I used the built in $SoftwareLinks = Get-StartApps which solved my first problem and then re-wrote the insert string to match the new format. Thank you everyone for your feedback.

How to get list of packages of a particular Visual Studio solution with nuget.exe command line

I would like to get a list of packages of my Visual Studio solution after I ran the nuget restore command.
How can I do it from command line or Powershell (oustide Visual Studio)?
You could run following PowerShell script to list all installed packages in your solution. Please modify the $SOLUTIONROOT as your solution path.
#This will be the root folder of all your solutions - we will search all children of this folder
$SOLUTIONROOT = "D:\Visual Studio 2015 Project\SO Case Sample\PackageSource"
Function ListAllPackages ($BaseDirectory)
{
Write-Host "Starting Package List - This may take a few minutes ..."
$PACKAGECONFIGS = Get-ChildItem -Recurse -Force $BaseDirectory -ErrorAction SilentlyContinue |
Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq "packages.config")}
ForEach($PACKAGECONFIG in $PACKAGECONFIGS)
{
$path = $PACKAGECONFIG.FullName
$xml = [xml]$packages = Get-Content $path
foreach($package in $packages.packages.package)
{
Write-Host $package.id
}
}
}
ListAllPackages $SOLUTIONROOT
Write-Host "Press any key to continue ..."
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

Create or Get Solution folder in NuGet Init.ps1

I am trying to create an Init.ps1 script for a NuGet package which adds items to a solution folder which may or may not already exist in the solution.
The script successfully adds or replaces the files in the filesystem and if there was no such solution folder, the items are successfully added to the newly created solution folder, however, if the solution folder already existed, a new solution folder named NewFolder1 (or 2 and on if that already exists) is created and no files are added to the solution.
param($installPath, $toolsPath, $package)
$solutionNode = Get-Interface $dte.Solution ([EnvDTE80.Solution2])
$solutionItemsNode = $solutionNode.FindProjectItem("FolderName")
$solutionItemsProjectItems = Get-Interface $solutionNode.ProjectItems ([EnvDTE.ProjectItems])
if (!$solutionItemsProjectItems) {
$solutionItemsNode = $solutionNode.AddSolutionFolder("FolderName")
$solutionItemsProjectItems = Get-Interface $solutionItemsNode.ProjectItems ([EnvDTE.ProjectItems])
}
$rootDir = (Get-Item $installPath).parent.parent.fullname
$deploySource = join-path $installPath '\sln\'
$deployTarget = join-path $rootDir '\FolderName\'
New-Item -ItemType Directory -Force -Path $deployTarget
ls $deploySource | foreach-object {
$targetFile = join-path $deployTarget $_.Name
Copy-Item $_.FullName $targetFile -Recurse -Force
$solutionItemsProjectItems.AddFromFile($targetFile) > $null
} > $null
I have tried various alterations such as checking $solutionItemsNode instead of $solutionItemsProjectItems (same effect), adding directly to solution instead of under solution folder (did nothing) and the following change which acts the same as the original:
$solutionItemsNode = $solution.Projects | where-object { $_.ProjectName -eq "FolderName" } | select -first 1
if (!$solutionItemsNode) {
$solutionItemsNode = $solutionNode.AddSolutionFolder("FolderName")
}
Any pointers?
Related Q&As and MSDN links:
adding-solution-level-items-in-a-nuget-package
nuget-how-to-add-files-to-solution-folder
copy-files-to-solution-folder-with-init-ps1-and-nuget
link from above answer
MSDN: envdte80.solutionfolder
When you add a solution folder to a solution it is available from the EnvDTE80.Solution2's Projects property.
So using $solutionNode.Projects seems to work. The only modification to the code you had is the use of $solutionNode.Projects instead of $solution.Projects since $solution does not exist anywhere so the result would always be $null.
$solutionItemsNode = $solutionNode.Projects | where-object { $_.ProjectName -eq "FolderName" } | select -first 1
if (!$solutionItemsNode) {
$solutionItemsNode = $solutionNode.AddSolutionFolder("FolderName")
}
The code above will not add the solution folder if it already exists.

Determine local path of a TFS Workspace via tf.exe

I am using a batch script for getting the latest version of specific projects. This script only runs tf.exe and gets the latest version of some Binaries. Everything works fine, but I would like to change the attrib of the downloaded files to be writeable (by deafult these files are read-only). For that I want to determine the local path of the files and use the attrib-command from batch.
tf.exe workfold [Workspace] shows me the local path in some kind of listing but it would be easier, if it only shows me what I want so I can use the prompt. Until now the it looks like this:
tf.exe workfold [Workspace]
=======================================
Arbeitsbereich: XYZ-xxxxxx (Username)
Auflistung: TFS-URL
[Workspace]: C:\xxx\TFS\xxx
Is it possible to determine only the local path mapping of a TFS Workspace so that I can use the prompt for the attrib-command without parsing?
What about the following (crude!!!) concept?
function Get-TfsWorkfold([string]$TfsCollection, [string]$TfsWorkspace)
{
$TfExePath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio 10.0\Common7\IDE\TF.exe"
Write-Output "Getting workfold for '$TfsCollection'->'$TfsWorkspace'..."
Push-Location $LocalPath
& "$TfExePath" workfold /collection:$TfsCollection /workspace:$TfsWorkspace
}
function Handle-Path()
{
param([Parameter(ValueFromPipeline=$true,Position=0)] [string] $line)
$startIndex = $line.IndexOf(': ') + 2;
$correctedLine = $line.subString($startIndex, $line.length - $startIndex - 1);
Write-Output $correctedLine;
Get-ChildItem $correctedLine
}
Get-TfsWorkfold "{serverAndcollection}" "{workspace}" > c:\temp\test.txt
Select-String c:\temp\test.txt -pattern:': ' | Select-Object Line | Handle-Path
The last line in Handle-Path is the example which you can rewirte with whatever you want to. It is PowerShell but it should work as you want.
Replace {serverAndcollection} and {workspace}.
Real men do it in one line
powershell -command "& {tf workfold | Select-String -pattern:' $' -SimpleMatch | Select-Object Line | ForEach-Object {$startIndex = $_.Line.IndexOf(': ') + 2; $_.Line.subString($startIndex, $_.Line.length - $startIndex - 1)}}"
Current answer will only return one last path if there are many.
You can also do it without any string manipulation, with calls to TF.exe. I have wrapped that in PowerShell scripts, so you get the following:
function Add-TfsTypes
{
# NOTE: Not all of the below are needed, but these are all the assemblies we load at the moment. Please note that especially NewtonSoft dll MUST be loaded first!
$PathToAssemblies = "C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer"
Add-Type -Path "$PathToAssemblies\NewtonSoft.Json.dll"
Add-Type -Path "$PathToAssemblies\System.Net.http.formatting.dll"
Add-Type -Path "$PathToAssemblies\Microsoft.TeamFoundation.Client.dll"
Add-Type -Path "$PathToAssemblies\Microsoft.TeamFoundation.Common.dll"
Add-Type -Path "$PathToAssemblies\Microsoft.TeamFoundation.VersionControl.Client.dll"
Add-Type -Path "$PathToAssemblies\Microsoft.TeamFoundation.WorkItemTracking.Client.dll"
}
function Get-TfsServerPathFromLocalPath {
param(
[parameter(Mandatory=$true)][string]$LocalPath,
[switch]$LoadTfsTypes
)
if ($LoadTfsTypes) {
Add-TfsTypes # Loads dlls
}
$workspaceInfo = [Microsoft.TeamFoundation.VersionControl.Client.Workstation]::Current.GetLocalWorkspaceInfo($LocalPath)
$server = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection $workspaceInfo.ServerUri
$workspace = $workspaceInfo.GetWorkspace($server)
return $workspace.GetServerItemForLocalItem($LocalPath)
}
The above method can then be called like this:
$serverFolderPath = Get-TfsServerPathFromLocalPath $folderPath -LoadTfsTypes
$anotherServerPath = Get-TfsServerPathFromLocalPath $anotherItemToTestPathOn

Resources