It appears that I can run all my tests in the solution in one go from the command line using MSTest if I use the /testmetadata flag as described here: http://msdn.microsoft.com/en-us/library/ms182487.aspx
I'm running SQL Server DB Unit tests in Visual Studio 2013, wherein I don't seem to have a vsmdi file at all, and I'm unable to find a way to add one either. I tried creating a testsettings file, but it doesn't discover any tests (shows "No tests to run") when I invoke MSTest.
Is there a way I can have MSTest run all my tests in a VS2013 solution?
I wanted to close this open question. My intention was to run all tests in one go from out Hudson CI server, so I wrote a basic console app to find and invoke MSTest on all DLL files inside the solution folder. This app is executed after the project is built in release mode.
string execId = null;
string className = null;
string testName = null;
string testResult = null;
string resultLine = null;
List<string> results = new List<string>();
XmlDocument resultsDoc = new XmlDocument();
XmlNode executionNode = null;
XmlNode testMethodNode = null;
// Define the test instance settings
Process testInstance = null;
ProcessStartInfo testInfo = new ProcessStartInfo()
{
UseShellExecute = false,
CreateNoWindow = true,
};
// Fetch project list from the disk
List<string> excluded = ConfigurationManager.AppSettings["ExcludedProjects"].Split(',').ToList();
DirectoryInfo assemblyPath = new DirectoryInfo(Assembly.GetExecutingAssembly().Location);
DirectoryInfo[] directories = assemblyPath.Parent.Parent.Parent.Parent.GetDirectories();
// Create a test worklist
List<string> worklist = directories.Where(t => !excluded.Contains(t.Name))
.Select(t => String.Format(ConfigurationManager.AppSettings["MSTestCommand"], t.FullName, t.Name))
.ToList();
// Start test execution
Console.WriteLine("Starting Execution...");
Console.WriteLine();
Console.WriteLine("Results Top Level Tests");
Console.WriteLine("------- ---------------");
// Remove any existing run results
if (File.Exists("UnitTests.trx"))
{
File.Delete("UnitTests.trx");
}
// Run each project in the worklist
foreach (string item in worklist)
{
testInfo.FileName = item;
testInstance = Process.Start(testInfo);
testInstance.WaitForExit();
if (File.Exists("UnitTests.trx"))
{
resultsDoc = new XmlDocument();
resultsDoc.Load("UnitTests.trx");
foreach (XmlNode result in resultsDoc.GetElementsByTagName("UnitTestResult"))
{
// Get the execution ID for the test
execId = result.Attributes["executionId"].Value;
// Find the execution and test method nodes
executionNode = resultsDoc.GetElementsByTagName("Execution")
.OfType<XmlNode>()
.Where(n => n.Attributes["id"] != null && n.Attributes["id"].Value.Equals(execId))
.First();
testMethodNode = executionNode.ParentNode
.ChildNodes
.OfType<XmlNode>()
.Where(n => n.Name.Equals("TestMethod"))
.First();
// Get the class name, test name and result
className = testMethodNode.Attributes["className"].Value.Split(',')[0];
testName = result.Attributes["testName"].Value;
testResult = result.Attributes["outcome"].Value;
resultLine = String.Format("{0} {1}.{2}", testResult, className, testName);
results.Add(resultLine);
Console.WriteLine(resultLine);
}
File.Delete("UnitTests.trx");
}
}
// Calculate passed / failed test case count
int passed = results.Where(r => r.StartsWith("Passed")).Count();
int failed = results.Where(r => r.StartsWith("Failed")).Count();
// Print the summary
Console.WriteLine();
Console.WriteLine("Summary");
Console.WriteLine("-------");
Console.WriteLine("Test Run {0}", failed > 0 ? "Failed." : "Passed.");
Console.WriteLine();
if (passed > 0)
Console.WriteLine("\tPassed {0,7}", passed);
if (failed > 0)
Console.WriteLine("\tFailed {0,7}", failed);
Console.WriteLine("\t--------------");
Console.WriteLine("\tTotal {0,8}", results.Count);
if (failed > 0)
Environment.Exit(-1);
else
Environment.Exit(0);
My App.config file:
<appSettings>
<add key="ExcludedProjects" value="UnitTests.Bootstrap,UnitTests.Utils" />
<add key="MSTestCommand" value=""c:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\MSTest.exe" /testcontainer:"{0}\bin\Release\{1}.dll" /nologo /resultsfile:"UnitTests.trx"" />
</appSettings>
MsTest is kind of the "deprecated" test framework of Visual Studio 2013. it's still used for some types of tests, but many of the other tests that can be executed now live in the new Agile Test Runner. Now SQL Database Unit Tests seems to still be in the category of "Some types" and need to be executed through MsTest.exe.
The easiest way is to use the /TestContainer commandline switch and use a naming pattern for your test projects. That way you can quickly grab all the assemblies with a specific naming pattern and then feed them to MsTest. Simple powershell command could be used to grab all the files that adhere to your pattern and then feed them to the commandline.
The vsmdi will still work in Visual Studio 2013, but the editor has been removed from the tool, plus there is no template for it anymore. So it's very hard to use. This is what Microsoft has to say about the VSDMI's:
Caution
Test lists are no longer fully supported in Visual Studio 2012:
You cannot create new test lists.
You cannot run test list tests from within Visual Studio.
If you upgraded from Visual Studio 2010, and had test list in your solution, you can continue to edit it in Visual Studio.
You can continue to run test list using mstest.exe from the command line, as described above.
If you were using a test list in your build definition, you can continue to use it.
Basically they're telling you to stop using this technique and use a combination of TestCategory's to create easy to execute tests groups.
As you can add multiple parameters for test containers, you can all group them into one call:
/testcontainer:[file name] Load a file that contains tests. You can
Specify this option more than once to
load multiple test files.
Examples:
/testcontainer:mytestproject.dll
/testcontainer:loadtest1.loadtest
MsTest /testcontainer:assemblyone.dll /testcontainer:assemblytwo.dll /testcontainer:assembly3.dll
To run MsTest on multiple assemblies at once. And do not (yet) make use of XUnit .NET or NUnit, as these cannot be combined into one report without switching to the new Agile test runner.
I don't know if this will help you or not, but I use Invoke-MsBuild. Its a PowerShell module that should does exactly what you need. I don't know if you were looking for a PowerShell solution or not, but it works great!
It also has a sister script, Invoke-MsTest for running MsTest instead of MsBuild.
Just for completeness, I often want to run tests as console application, simply because I find it much easier to debug this for some reason... Over the years I've created a few small test helpers to help me out; I suppose you can use them with your CI solution quite easily.
I understand this isn't your question entirely; however, since you're looking for a CI solution and mention visual studio, this should solve it quite nicely.
Just to let you know, my little framework is a bit bigger than this, but the things that are missing are quite easy to add. Basically what I left out is everything for logging and the fact that I test different assemblies in different app domains (because of possible DLL conflicts and state). More on that below.
One thing to notice is that I do not catch any exceptions in the process below. My main focus is making it easy to debug your application when troubleshooting. I have a separate (but similar) implementation for CI that basically adds try/catches on the comment points below.
There's only one catch to this method: Visual Studio won't copy all the assemblies you reference; it'll only copy assemblies that you use in the code. A simple workaround for this is to introduce a method (which is never called) which uses one type in the DLL you're testing. That way, your assembly will be copied and everything will work nicely.
Here's the code:
static class TestHelpers
{
public static void TestAll(this object o)
{
foreach (MethodInfo meth in o.GetType().GetMethods().
Where((a) => a.GetCustomAttributes(true).
Any((b) => b.GetType().Name.Contains("TestMethod"))))
{
Console.WriteLine();
Console.WriteLine("--- Testing {0} ---", meth.Name);
Console.WriteLine();
// Add exception handling here for your CI solution.
var del = (Action)meth.CreateDelegate(typeof(Action), o);
del();
// NOTE: Don't use meth.Invoke(o, new object[0]); ! It'll eat your exception!
Console.WriteLine();
}
}
public static void TestAll(this Assembly ass)
{
HashSet<AssemblyName> visited = new HashSet<AssemblyName>();
Stack<Assembly> todo = new Stack<Assembly>();
todo.Push(ass);
HandleStack(visited, todo);
}
private static void HandleStack(HashSet<AssemblyName> visited, Stack<Assembly> todo)
{
while (todo.Count > 0)
{
var assembly = todo.Pop();
// Collect all assemblies that are related
foreach (var refass in assembly.GetReferencedAssemblies())
{
TryAdd(refass, visited, todo);
}
foreach (var type in assembly.GetTypes().
Where((a) => a.GetCustomAttributes(true).
Any((b) => b.GetType().Name.Contains("TestClass"))))
{
// Add exception handling here for your CI solution.
var obj = Activator.CreateInstance(type);
obj.TestAll();
}
}
}
public static void TestAll()
{
HashSet<AssemblyName> visited = new HashSet<AssemblyName>();
Stack<Assembly> todo = new Stack<Assembly>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
TryAdd(assembly.GetName(), visited, todo);
}
HandleStack(visited, todo);
}
private static void TryAdd(AssemblyName ass, HashSet<AssemblyName> visited, Stack<Assembly> todo)
{
try
{
var reference = Assembly.Load(ass);
if (reference != null &&
!reference.GlobalAssemblyCache && // Ignore GAC
reference.FullName != null &&
!reference.FullName.StartsWith("ms") && // mscorlib and other microsoft stuff
!reference.FullName.StartsWith("vshost") && // visual studio host process
!reference.FullName.StartsWith("System")) // System libraries
{
if (visited.Add(reference.GetName())) // We don't want to test assemblies twice
{
todo.Push(reference); // Queue assembly for processing
}
}
}
catch
{
// Perhaps log something here... I currently don't because I don't care...
}
}
}
How to use this code:
You can simply call TestHelpers.TestAll() to test all assemblies, referenced assemblies, indirectly referenced assemblies, etc. This is probably what you want to do in CI.
You can call TestHelpers.TestAll(assembly) to test a single assembly with all referenced assemblies. This can be useful when you're splitting tests across multiple assemblies and/or when your debugging.
You can call new MyObject().TestAll() to invoke all tests in a single object. This is particularly helpful when debugging.
If you're using appdomains like me, you should make a single appdomain for a DLL you load dynamically from a folder and use TestAll on that. Also, if you use a scratch folder, you might want to empty that between tests. That way, multiple test framework versions and multiple tests won't interact with each other. Particularly if your tests use state (e.g. static variables), this might be a good practice. There are tons of examples for CreateInstanceAndUnwrap online that will help you out with this.
One thing to note is that I use a delegate instead of the method.Invoke. This basically means your exception object won't be eaten by Reflection, which means your debugger won't be broken. Also note that I check attributes by name, which means this will work with different frameworks - as long as the attribute names match.
HTH
Read the Caution part in your own link. It's no longer supported in VST 2012 the way you do it.
Maybe this update version can help:
http://msdn.microsoft.com/en-us/library/ms182490.aspx
Related
I have a plugin architecutre which is using a Shared DLL to implement a plugin interface and a base plugin class (eg. IPlugin & BasePlugin). The shared DLL is used by both the Plugin and the main application.
For some reason, when I try to cast an object that was instantiated by the main application to the shared plugin interface (IPlugin), I get an InvalidCastException saying I cannot cast this interface.
This is despite:
The class definitely implementing the plugin interface.
The Visual Studio debugger saying that "(objInstance is IPlugin)" is true when I mouse-over the statement, despite the same 'if' condition evaluating as false when I step-through the code or run the .exe.
The Visual Studio Immediate Window also confirms that the above condition is true and that is it possible to cast the object successfully.
I have cleaned/deleted all bin folders and also tried both VS2019 and VS2022 with exactly the same outcome.
I am going a little crazy here because I assume it is something to do with perhaps with multiple references of the same DLL somehow causing the issue (like this issue). The fact that the debugger tells me everything is okay makes it hard to trouble-shoot. I'm not sure if it's relevant but I have provided example code and the file structure below:
Shared.dll code
public interface IPlugin
{
}
public class BasePlugin : IPlugin
{
}
Plugin.dll code
class MyPlugin : BasePlugin
{
void Init()
{
// The plugin contains references to the main app dlls as it requires
// to call various functions inside the main app such as adding menu items etc
}
}
Main.exe
(Note: This is pseudo-code only and does not show the plugin framework used to load the plugin DLLs via Assembly.Load())
var obj = Activator.CreateInstance(pluginType); // Plugin type will be 'MyPlugin'
if (obj is IPlugin) // <== This executes as false when executed but evaluates to true in the Visual Studio debugger
{
}
var castObj = (IPlugin)obj // <== This will cause an invalid cast exception
Folder structure
|--- MainApp.exe
|--- Shared.dll
|--- Addins
|------- Plugin.dll
|------- Shared.dll
Does anyone know the reasons how I can trouble-shoot this issue and what might be causing it?
My suggestion is that (using the Properties\Signing tab) you sign your shared dll assembly with a Strong Name Key. If your plugin classes reference a signed copy of the dll, there should be no possibility of confusion.
The application has no prior knowledge of what the plugins are going to be, but it does know that it's going to support IPlugin. Therefore, I would reference the PlugInSDK project directly in the app project.
Testbench
Here's what works for me and maybe by comparing notes we can get to the bottom of it.
static void Main(string[] args)
{
Confirm that the shared dll (known here as PlugInSDK.dll) has already been loaded and display the source location. In the Console output, note the that PublicKeyToken is not null for the SDK assembly (this is due to the snk signing).
var sdk =
AppDomain.CurrentDomain.GetAssemblies()
.Single(asm=>(Path.GetFileName(asm.Location) == "PlugInSDK.dll"));
Console.WriteLine(
$"'{sdk.FullName}'\nAlready loaded from:\n{sdk.Location}\n");
The AssemblyLoad event provides the means to examine on-demand loads as they occur:
AppDomain.CurrentDomain.AssemblyLoad += (sender, e) =>
{
var name = e.LoadedAssembly.FullName;
if (name.Split(",").First().Contains("PlugIn"))
{
Console.WriteLine(
$"{name}\nLoaded on-demand from:\n{e.LoadedAssembly.Location}\n");
}
};
It doesn't matter where the plugins are located, but when you go to discover them I suggest SearchOption.AllDirectories because they often end up in subs like netcoreapp3.1.
var pluginPath =
Path.Combine(
Path.GetDirectoryName(Assembly.GetEntryAssembly().Location),
"..",
"..",
"..",
"PlugIns"
);
Chances are good that a copy of PlugInSDK.dll is going to sneak into this directory. Make sure that the discovery process doesn't accidentally pick this up. Any other Type that implements IPlugin gets instantiated and put in the list.
List<IPlugin> plugins = new List<IPlugin>();
foreach (
var plugin in
Directory.GetFiles(pluginPath, "*.dll", SearchOption.AllDirectories))
{
// Exclude copy of the SDK that gets put here when plugins are built.
if(Path.GetFileName(plugin) == "PlugInSDK.dll") continue;
// Be sure to use 'LoadFrom' not 'Load'
var asm = Assembly.LoadFrom(plugin);
// Check to make sure that any given *.dll
// implements IPlugin before adding to list.
Type plugInType =
asm.ExportedTypes
.Where(type =>
type.GetTypeInfo().ImplementedInterfaces
.Any(intfc => intfc.Name == "IPlugin")
).SingleOrDefault();
if ((plugInType != null) && plugInType.IsClass)
{
plugins.Add((IPlugin)Activator.CreateInstance(plugInType));
}
}
Now just display the result of the plugin discovery.
Console.WriteLine("LIST OF PLUGINS");
Console.WriteLine(
string.Join(
Environment.NewLine,
plugins.Select(plugin => plugin.Name)));
Console.ReadKey();
}
Is this something you're able to repro on your side?
I am using NUnit to test one functionality where I need to load XML file to object. The XML file is in location of the Console Application.
I have Following method where configuration will be read :
public string GetConfiguration(TempFlexProcessor processor)
{
var exePath = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location);
var configPath = Path.Combine(Path.GetFullPath(exePath), "configuration");
var configFile = string.Format(#"{0}.xml", processor.GetType().Name);
}
Now in my NUnit Test I have test method where I test GetConfiguration :
[Test]
public void TempFlexProcessorExecuteTest()
{
#region Given
#endregion
#region When
var tempFlexProcessor = new TempFlexProcessor();
var actual = tempFlexProcessor.GetConfiguration(tempFlexProcessor);
#endregion
Assert.AreEqual("path of the file", actual);
}
But System.Reflection.Assembly.GetEntryAssembly() is null, please help.
I used AppDomain.CurrentDomain.BaseDirectory instead of System.Reflection.Assembly.GetEntryAssembly().Location
I suspect the problem is that NUnit is running your tests in a different AppDomain, but without using ExecuteAssembly. From the documentation for Assembly.GetEntryAssembly:
Gets the process executable in the default application domain. In other application domains, this is the first executable that was executed by AppDomain.ExecuteAssembly.
It's not clear which assembly you really want to get - even if this did return something "appropriate" for NUnit, that's likely to be the NUnit executable, which would be well away from any configuration directories you happen to have.
Basically, I think that you should at least provide an alternative way of specifying the configuration directory - and you might want to reconsider whether using GetEntryAssembly is a good idea anyway. (Aside from anything else, it's slightly odd that you're calling GetConfiguration on a processor and passing in another processor... that may be suitable for your design, but it's at least somewhat unusual, given that in your test case you're passing in a reference to the same object.)
We are using TeamCity for continuous integration, our source control is Git, and we have 1 major repository that contains multiple .sln files (around 10).
All in all, this repository has about ~ 100 - 200 C# projects.
Upon a push to the master repository, TeamCity triggers a build that will compile all projects in the repository.
I'd like to be able to tell which projects were actually affected by a particular commit, and thus publish only those projects' outputs as artifacts of the current build.
For this, i've designed a solution to integrate NDepend into our build process, and generate a diff report between current and latest build outputs.
The outputs that were changed/added will be published as the build outputs.
I have little experience with NDepend; from what i've seen all of its true power comes from the query language that is baked into it.
I am wondering how (if possible) i can achieve the following:
Diff between a folder containing previous build's outputs and current folder of build outputs.
Have NDepend generate a report in a consumable format so i can determine the files that need to be copied.
Is this scenario possible? How easy/hard would that be?
So the simple answer is to do the Reporting Code Diff way as explained in this documentation. The problem with this basic answer is that, it pre-suppose two NDepend projects that always refers to the two same set of assemblies.
Certainly, the number and names of assemblies is varying in your context so we need to build two projects (old/new) on the fly and analyze them through NDepend.API.
Here is the NDepend.API source code for that. For a It-Just-Works experience, in the PowerTools source code (in $NDependInstallDir$\NDepend.PowerTools.SourceCode\NDepend.PowerTools.sln) just call the FoldersDiff.Main(); method after the AssemblyResolve registration call, in Program.cs.
...
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolverHelper.AssemblyResolveHandler;
FoldersDiff.Main();
...
Here is the the source code that harnesses NDepend.API.
Note that so much more can be done, through the two codeBase objects and the compareContext object. Instead of just showing the 3 lists of assemblies added/removed/codeWasChanges, you could show API breakings changes, new methods and types added, modified classes and methods, code quality regression... For that, just look at default code rules concerning diff, that are based on the same NDepend.CodeModel API.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NDepend;
using NDepend.Analysis;
using NDepend.CodeModel;
using NDepend.Path;
using NDepend.Project;
class FoldersDiff {
private static readonly NDependServicesProvider s_NDependServicesProvider = new NDependServicesProvider();
internal static void Main() {
var dirOld = #"C:\MyProduct\OldAssembliesDir".ToAbsoluteDirectoryPath();
var dirNew = #"C:\MyProduct\NewAssembliesDir".ToAbsoluteDirectoryPath();
Console.WriteLine("Analyzing assemblies in " + dirOld.ToString());
var codeBaseOld = GetCodeBaseFromAsmInDir(dirOld, TemporaryProjectMode.TemporaryOlder);
Console.WriteLine("Analyzing assemblies in " + dirNew.ToString());
var codeBaseNew = GetCodeBaseFromAsmInDir(dirNew, TemporaryProjectMode.TemporaryNewer);
var compareContext = codeBaseNew.CreateCompareContextWithOlder(codeBaseOld);
// So much more can be done by exploring fine-grained diff in codeBases and compareContext
Dump("Added assemblies", codeBaseNew.Assemblies.Where(compareContext.WasAdded));
Dump("Removed assemblies", codeBaseOld.Assemblies.Where(compareContext.WasRemoved));
Dump("Assemblies with modified code", codeBaseNew.Assemblies.Where(compareContext.CodeWasChanged));
Console.Read();
}
internal static ICodeBase GetCodeBaseFromAsmInDir(IAbsoluteDirectoryPath dir, TemporaryProjectMode temporaryProjectMode) {
Debug.Assert(dir.Exists);
var dotNetManager = s_NDependServicesProvider.DotNetManager;
var assembliesPath = dir.ChildrenFilesPath.Where(dotNetManager.IsAssembly).ToArray();
Debug.Assert(assembliesPath.Length > 0); // Make sure we found assemblies
var projectManager = s_NDependServicesProvider.ProjectManager;
IProject project = projectManager.CreateTemporaryProject(assembliesPath, temporaryProjectMode);
// In PowerTool context, better call:
// var analysisResult = ProjectAnalysisUtils.RunAnalysisShowProgressOnConsole(project);
var analysisResult = project.RunAnalysis();
return analysisResult.CodeBase;
}
internal static void Dump(string title, IEnumerable<IAssembly> assemblies) {
Debug.Assert(!string.IsNullOrEmpty(title));
Debug.Assert(assemblies != null);
Console.WriteLine(title);
foreach (var #assembly in assemblies) {
Console.WriteLine(" " + #assembly.Name);
}
}
}
I am working on a small Visual Studio extension that acts on projects in a solution based on if they are set to build in the active build configuration or not. The problem I am having is that I cannot figure out how to determine what those projects are.
I have implemented IVsUpdateSolutionEvents, in which I implement OnActiveProjectCfgChange. I can get Visual Studio to enter the code block when I change configurations, and I have been able to get it to do many of the things that I would like to do, but without being able to determine what projects should be built in the active configuration, I am dead in the water.
My implementation so far is:
public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
{
var activeProjects = new HashSet<string>(); // TODO: Get projects in active configuration
foreach (Project project in _dte.Solution.Projects)
{
if (project.Kind != "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}" // C#
&& project.Kind != "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}" // VB
&& project.Kind != "{13B7A3EE-4614-11D3-9BC7-00C04F79DE25}" // VSA
)
continue;
IVsHierarchy projectHierarchy;
_solutionService.GetProjectOfUniqueName(project.UniqueName, out projectHierarchy);
if (activeProjects.Contains(project.UniqueName))
{
// Project is to be built
}
else
{
// Project is not to be built
}
return VSConstants.S_OK;
}
}
What I need to do is figure out how to fill in the HashSet at the beginning of the function. (Marked with TODO). I have searched and searched, but I have not found what I need.
Does anybody have any references to documentation or sample code that might help me move forward?
You can use SolutionContext.ShouldBuild property
foreach (SolutionContext solutionContext in _applicationObject.Solution.SolutionBuild.ActiveConfiguration.SolutionContexts)
{
if (solutionContext.ShouldBuild)
activeProjects.Add(solutionContext.ProjectName);
}
I'm looking for a method that let's me validate code and generator code as part of the build process, using Visual Studio 2010 (not express) and MSBuild.
Background Validation:
I'm writing a RESTful web service using the WCF Web Api. Inside the service class that represents the web service I have to define an endpoint, declaring additionally parameters as plain test. When the parameter name inside the endpoint declaration differs from the parameter of the C# method I get a error - unfortunately at run time when accessing the web service, not at compile time. So I thought it would be nice to analyze the web service class as part of the compile step for flaws like this, returning an error when something is not right.
Example:
[WebGet(UriTemplate = "Endpoint/{param1}/{param2}")]
public string MyMethod(string param1, string parameter2) {
// Accessing the web service now will result in an error,
// as there's no fitting method-parameter named "param2".
}
Also I'd like to enforce some naming rules, such as GET-Methods must start with the "Get" word. I believe this will help the service to remain much more maintainable when working with several colleagues.
Background Generation:
I will be using this REST web service in a few other projects, there for I need to write a client to access this service. But I don't want to write a client for each of these, always adjusting whenever the service changes. I'd like the clients to be generated automatically, based upon the web service code files.
Previous approach:
So far I tried to use a T4 template using the DTE interface to parse the code file and validate it, or generate the client. This worked fine in Visual Studio when saving manually, but integrating this in the build process turned out to be not so working well, as the Visual Studio host is not available using MSBuild.
Any suggestion is welcome. :)
Instead of using DTE or some other means to parse the C# code you could use reflection (with Reflection-Only context) to examine the assembly after it's compiled. Using reflection is a more robust solution and probably faster also (especially if you use Mono.Cecil to do the reflecting).
For the MSBuild integration I would recommend writing a custom MSBuild task - it's fairly easy and more robust/elegant than writing a command line utility that's executed by MSBuild.
This may be a long shot but still qualifies as "any suggestion" :)
You could compile the code, then run a post-build command which would be a tool that you'd have to write which uses reflection to compare the parsed UriTemplate text with the method parameter names, catching errors and outputting them in a manner that MSBuild will pickup. Look at This Link for information on how to output so MSBuild will put the errors in the visual studio error list. The post-build tool could then delete the compiled assemblies if errors were found, thus "simulating" a failed build.
Here's the SO Link that lead me to the MSBuild Blog too, just for reference.
HTH
For the enforcement side of things, custom FxCop rules would probably be a very good fit.
For the client code generation, there are quite a few possibilities. If you like the T4 approach, there is probably a way to get it working with MSBuild (but you would definitely need to provide a bit more detail regarding what isn't working now). If you're want an alternative anyway, a reflection-based post-build tool is yet another way to go...
Here is a short, extremely ugly program that you can run over an assembly or group of assemblies (just pass the dlls as arguments) to perform the WebGet UriTemplate check. If you don't pass anything, it runs on itself (and fails, appropriately, as it is its own unit test).
The program will print out to stdout the name of the methods that are missing the parameters and the names of the missing parameters, and if any are found, will return a non-zero return code (standard for a program failing), making it suitable as a post-build event. I am not responsible if your eyes bleed:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.ServiceModel.Web;
namespace ConsoleApplication1
{
class Program
{
static int Main(string[] args)
{
var failList = new ConcurrentDictionary<MethodInfo, ISet<String>>();
var assembliesToRunOn = (args.Length == 0 ? new[] {Assembly.GetExecutingAssembly()} : args.Select(Assembly.LoadFrom)).ToList();
assembliesToRunOn.AsParallel().ForAll(
a => Array.ForEach(a.GetTypes(), t => Array.ForEach(t.GetMethods(BindingFlags.Public | BindingFlags.Instance),
mi =>
{
var miParams = mi.GetParameters();
var attribs = mi.GetCustomAttributes(typeof (WebGetAttribute), true);
if (attribs.Length <= 0) return;
var wga = (WebGetAttribute)attribs[0];
wga.UriTemplate
.Split('/')
.ToList()
.ForEach(tp =>
{
if (tp.StartsWith("{") && tp.EndsWith("}"))
{
var tpName = tp.Substring(1, tp.Length - 2);
if (!miParams.Any(pi => pi.Name == tpName))
{
failList.AddOrUpdate(mi, new HashSet<string> {tpName}, (miv, l) =>
{
l.Add(tpName);
return l;
});
}
}
});
})));
if (failList.Count == 0) return 0;
failList.ToList().ForEach(kvp => Console.Out.WriteLine("Method " + kvp.Key + " in type " + kvp.Key.DeclaringType + " is missing the following expected parameters: " + String.Join(", ", kvp.Value.ToArray())));
return failList.Count;
}
[WebGet(UriTemplate = "Endpoint/{param1}/{param2}")]
public void WillPass(String param1, String param2) { }
[WebGet(UriTemplate = "Endpoint/{param1}/{param2}")]
public void WillFail() { }
[WebGet(UriTemplate = "Endpoint/{param1}/{param2}")]
public void WillFail2(String param1) { }
}
}