13

You have a large Visual Studio Solution with dozens of project files in it. How would you verify that all the projects follow certain rules in their property settings, and enforce these rules if a new project is added. For example check that all projects have:

TargetFrameworkVersion = "v4.5"
Platform = "AnyCPU"
WarningLevel = 4
TreatWarningsAsErrors = true
OutputPath = $(SolutionDir)bin
SignAssembly = true
AssemblyName = $(ProjectFolderName)

I know two methods myself that I will add in an answer below, but I was wondering how people go about doing this type of project test. I'm especially interested to learn about available solutions such as libraries or build tasks for this rather than having to have to invent something new or write it from scratch.

orad
  • 15,272
  • 23
  • 77
  • 113
  • Did you consider including one single common file in all projects, with the settings you mention? That would already reduce the chance anything is off. In any case the only way to be 100% sure is parse all projects and check settings - you can write MsBuild code to do that (something like in your answer, but then automated so it runs automatically for each project and without the need to modify projects) or use the classes in the `Microsoft.Build.Evaluation` namespace to write a tool in e.g. C# to do that. – stijn Oct 10 '15 at 17:21
  • The *.*proj files are XML, you can write a program to find any violations of your rules, then take whatever appropriate action. You can also wire this into your CI framework of choice. – Chris O Oct 11 '15 at 21:10
  • Did you end up finding a solution for this? I have a similar problem where I want to track different versions of dll's used in packages. – Sam Nov 30 '15 at 12:54
  • @Sam nope, but I had some [discussion](https://github.com/ligershark/psbuild/issues/75) with Ibrahim Hashimi, the author of [PSBuild](https://github.com/ligershark/psbuild) maybe we could improve that tool to support this kind of verification using powershell. – orad Dec 01 '15 at 00:49

10 Answers10

9

*.sln files are plain text and easily parsable, and *.*proj files are xml.

You can add a dummy project with a prebuild step that parses the sln to retrieve all of the project files, validate their settings, print a report, and fail the build if necessary.

Also, you should check this post to ensure the prebuild step is always executed. Essentially, you specify a blank output in the custom build step to force a rebuild.

Community
  • 1
  • 1
kidmosey
  • 400
  • 2
  • 9
6

The following list identifies the key file types that are automatically added to VSS when a solution is added to source control by using the Visual Studio .NET integrated development environment (IDE):

Solution files (.sln). The key items maintained within these files include a list of constituent projects, dependency information, build configuration details, and source control provider details. Project files (.csproj or *.vbproj). The key items maintained within these files include assembly build settings, referenced assemblies (by name and path), and a file inventory. Application configuration files. These are configuration files based on Extensible Markup Language (XML) used to control various aspects of your project's run time behavior.

Use a Single Solution Model Whenever Possible an

Also see : https://msdn.microsoft.com/en-us/library/ee817677.aspx, https://msdn.microsoft.com/en-us/library/ee817675.aspx

AND For CONTINUOUS INTEGRATION : there are many tools available like MSBuild, Jenkins, Apache's Continuum, Cruise Control (CC), and Hudson(plugin can be extended to c#)

Ryu
  • 191
  • 10
  • We use a Single Solution Model. The question is, when the solution consists of 30 projects, how do you make sure that all of them have consistent configuration settings in them, such as targeting the same processor architecture, being stored in the same folder name as the AssemblyName, all treating warnings as errors, etc? – orad Oct 15 '15 at 16:46
4

This is what I have myself:

One way to do this is to create an MSBuild target with error conditions:

<Error Condition="'$(TreatWarningsAsErrors)'!='true'" Text="Invalid project setting" />

I like this approach because it is integrated with MSBuild and gives you early errors, however, you have to modify every project to import it in them or get all your team members to use a special command prompt with environment variables that will inject custom pre-build steps into your projects during the build, which is a pain.

The second approach I know is to use some library like VSUnitTest which provides an API to project properties that you can test against. VSUnitTest is currently not open source and unlisted from the NuGet service.

orad
  • 15,272
  • 23
  • 77
  • 113
4

You could write some code to open the the solution as a text file to identify all of the csproj files referenced, in turn opening each of these as xml files, and then writing unit tests to ensure specific nodes of the project match what you expect.

It's a quick and dirty solution, but works for CI and gives you the flexibility to ignore nodes you don't care about. It actually sounds kinda useful. I have a solution with 35 projects I'd like to scan too.

Sam
  • 669
  • 1
  • 13
  • 26
4

Let's try something completely different: you could ensure that they are consistent by construction by generating them from a template or by using a build generation tool such as CMake. This might be simpler than attempting to make them consistent after the fact.

Richard Cook
  • 32,523
  • 5
  • 46
  • 71
4

In our work we use a powershell script that checks project settings and modified them if they are incorrect. For example, we remove Debug configuration this way, disable C++ optimization and SSE2 support. We run it manually, but definitely it is possible to run it automatically, e.g. as pre\post build step.

Below the example:

`function Prepare-Solution {  
param (  
    [string]$SolutionFolder
)  
$files = gci -Recurse -Path $SolutionFolder -file *.vcxproj | select -    ExpandProperty fullname  
$files | %{  
    $file = $_  
    [xml]$xml = get-content $file  

    #Deleting Debug configurations...
    $xml.Project.ItemGroup.ProjectConfiguration | ?{$_.Configuration -eq "Debug"} | %{$_.ParentNode.RemoveChild($_)} | Out-Null
    $xml.SelectNodes("//*[contains(@Condition,'Debug')]") |%{$_.ParentNode.RemoveChild($_)} | Out-Null

    if($xml.Project.ItemDefinitionGroup.ClCompile) {  
        $xml.Project.ItemDefinitionGroup.ClCompile | %{  
            #Disable SSE2
            if (-not($_.EnableEnhancedInstructionSet)){
                $_.AppendChild($xml.CreateElement("EnableEnhancedInstructionSet", $xml.DocumentElement.NamespaceURI)) | Out-Null  
            }   

            if($_.ParentNode.Condition.Contains("Win32")){  
                $_.EnableEnhancedInstructionSet = "StreamingSIMDExtensions"
            }
            elseif($_.ParentNode.Condition.Contains("x64")) {
                $_.EnableEnhancedInstructionSet = "NotSet"
            } else {
                Write-Host "Neither x86 nor x64 config. Very strange!!"
            }

            #Disable Optimization
            if (-not($_.Optimization)){  
                $_.AppendChild($xml.CreateElement("Optimization", $xml.DocumentElement.NamespaceURI)) | Out-Null  
            }   
            $_.Optimization = "Disabled" 
        } 
    } 
    $xml.Save($file);  
} }`
olegk
  • 765
  • 4
  • 13
3

A file is an assembly if and only if it is managed, and contains an assembly entry in its metadata. For more information on assemblies and metadata, see the topic Assembly Manifest.

How to manually determine if a file is an assembly

  1. Start the Ildasm.exe (IL Disassembler).
  2. Load the file you wish to test.
  3. If ILDASM reports that the file is not a portable executable (PE) file, then it is not an assembly.
    For more information, see the topic How to: View Assembly Contents.

How to programmatically determine if a file is an assembly

  1. Call the GetAssemblyName method, passing the full file path and name of the file you are testing.
  2. If a BadImageFormatException exception is thrown, the file is not an assembly.

This example tests a DLL to see if it is an assembly.

class TestAssembly
{
static void Main()
   {

    try
    {
        System.Reflection.AssemblyName testAssembly = System.Reflection.AssemblyName.GetAssemblyName(@"C:\Windows\Microsoft.NET\Framework\v3.5\System.Net.dll");

        System.Console.WriteLine("Yes, the file is an assembly.");
    }

    catch (System.IO.FileNotFoundException)
    {
        System.Console.WriteLine("The file cannot be found.");
    }

    catch (System.BadImageFormatException)
    {
        System.Console.WriteLine("The file is not an assembly.");
    }

    catch (System.IO.FileLoadException)
    {
        System.Console.WriteLine("The assembly has already been loaded.");
    }
   }
}
  // Output (with .NET Framework 3.5 installed):
 // Yes, the file is an assembly.

Framework is the highest installed version, SP is the service pack for that version.

  RegistryKey installed_versions =   Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP");
  string[] version_names = installed_versions.GetSubKeyNames();
  //version names start with 'v', eg, 'v3.5' which needs to be trimmed off    before conversion
  double Framework = Convert.ToDouble(version_names[version_names.Length - 1].Remove(0, 1), CultureInfo.InvariantCulture);
  int SP =  Convert.ToInt32(installed_versions.OpenSubKey(version_names[version_names.Length     - 1]).GetValue("SP", 0));

 For .Net 4.5


 using System;
 using Microsoft.Win32;


 ...


 private static void Get45or451FromRegistry()
{
using (RegistryKey ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine,    RegistryView.Registry32).OpenSubKey("SOFTWARE\\Microsoft\\NET Framework  Setup\\NDP\\v4\\Full\\")) {
    int releaseKey = Convert.ToInt32(ndpKey.GetValue("Release"));
    if (true) {
        Console.WriteLine("Version: " + CheckFor45DotVersion(releaseKey));
     }
   }
 }


 ...


// Checking the version using >= will enable forward compatibility,  
// however you should always compile your code on newer versions of 
// the framework to ensure your app works the same. 
private static string CheckFor45DotVersion(int releaseKey)
{
if (releaseKey >= 393273) {
   return "4.6 RC or later";
}
if ((releaseKey >= 379893)) {
    return "4.5.2 or later";
}
if ((releaseKey >= 378675)) {
    return "4.5.1 or later";
}
if ((releaseKey >= 378389)) {
    return "4.5 or later";
}
// This line should never execute. A non-null release key should mean 
// that 4.5 or later is installed. 
return "No 4.5 or later version detected";
}
H. Pauwelyn
  • 13,575
  • 26
  • 81
  • 144
Manraj
  • 496
  • 2
  • 15
  • Thanks but this doesn't answer the question. We're not dealing with any assemblies. Project files are xml files basically, and we're looking for a solution other than processing them manually. – orad Oct 13 '15 at 15:54
  • With some helper library maybe. I suppose this problem is not specific to me. In fact every large project needs to have this test. If I have to create a solution from scratch then I'd probably write a library for it for everybody to use. – orad Oct 15 '15 at 16:39
  • @orad if you want to do so then you have to make dll which may visual studio process at debugging – Manraj Oct 17 '15 at 05:25
1

You could go the search & replace Regex way with a handwritten C#, Script, powershell or similar. But it has the following problems:

  • Difficult to read (Read your pretty regex in three or more months)
  • Difficult to enhance(New regex for new search/replace/check feature)
  • Easy to break (a new release/format of ms build project or a not forecast tag may not work)
  • Harder to test (you must check that no unintended match occurs)
  • Difficult to maintain (because of the above)

and the following advantages:

  • Not doing any extra validation which (may) let it work on any kind of project (mono or visual).
  • Doesn't care about \r :)

The best could be to use the Microsoft.Build.Evaluation and build a C# tool which does all your testing/checking/fix and so on.

I've done a command line tool that use a sourcefile list (used by Mono) and update sources of csproj and another which dumps on console the csproj content. It was easy to do, pretty straightforward and easy to test also.

However, it may fail (as I've experienced it) on projects modified by "non" Ms tool (like Mono Studio) or because of missing \r.... Anyway, you can always handle it with an exception catch and a good message.

Here a sample by using Microsoft.Build.dll (don't use Microsof.Build.Engine as it is obsolete):

using System;
using Microsoft.Build.Evaluation;

internal class Program
{
    private static void Main(string[] args)
    {
        var project = new Project("PathToYourProject.csproj");
        Console.WriteLine(project.GetProperty("TargetFrameworkVersion", true, string.Empty));
        Console.WriteLine(project.GetProperty("Platform", true, string.Empty));
        Console.WriteLine(project.GetProperty("WarningLevel", true, string.Empty));
        Console.WriteLine(project.GetProperty("TreatWarningsAsErrors", true, "false"));
        Console.WriteLine(project.GetProperty("OutputPath", false, string.Empty));
        Console.WriteLine(project.GetProperty("SignAssembly", true, "false"));
        Console.WriteLine(project.GetProperty("AssemblyName", false, string.Empty));
        Console.ReadLine();
    }
}

public static class ProjectExtensions
{
    public static string GetProperty(this Project project, string propertyName, bool afterEvaluation, string defaultValue)
    {
        var property = project.GetProperty(propertyName);
        if (property != null)
        {
            if (afterEvaluation)
                return property.EvaluatedValue;
            return property.UnevaluatedValue;
        }
        return defaultValue;
    }
}
Fab
  • 14,327
  • 5
  • 49
  • 68
1

For similar purposes we use custom MSBuild fragments with common properties that we want to share between the projects, like this (build.common.props file):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
    <PlatformToolset>v90</PlatformToolset>
    <OutputPath>$(SolutionDir)..\bin\$(PlatformPath)\$(Configuration)\</OutputPath>

   <!-- whatever you need here -->
  </PropertyGroup>

</Project>

And then we just include this fragment to real VS projects we want to apply these properties to:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <CommonProps>$(SolutionDir)..\Build\build.common.props</CommonProps>
  </PropertyGroup>

  <Import Project="$(CommonProps)" />

  <!-- the rest of the project -->

</Project>

We handle a lot of things using this approach:

  • common properties, as you mentioned
  • static analysis (FxCop, StyleCop)
  • digital sign of assemblies
  • etc.

The only disadvantage that you need to include these MSBuild fragments into each project file, but once you do that, you have all the benefits of modular build system that is easy to manage and update.

  • There is a way to avoid adding this to every project. Put your custom MSBuild code to a separate **.proj** file then use the **CustomBeforeMicrosoftCommonTargets** environment variable to point to it. – orad Oct 19 '15 at 18:35
  • That is a bit of extreme, in my opinion. With environment variable you add the separate .proj file into **every** MSBuild project in your solution. Which means that it is extremely difficult to handle various types of projects (C#, C++, Sandcastle) and different per-project settings. Besides, it doesn't work from within VS unless environment variable is global on user's machine. – Dmitry Kolomiets Oct 29 '16 at 10:23
0

I also faced this issue and created a small solution that creates a csv file with details to identifies the inconsistences. You can look at it in this url

https://github.com/gdlmanuv/VSProjectConsistencyChecker

Manuel
  • 21
  • 4
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Pankwood Feb 18 '22 at 18:23