Build-time code validation and generation based upon code files across projects

1.8k views Asked by At

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. :)

4

There are 4 answers

0
Pent Ploompuu On BEST ANSWER

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.

0
Erikest On

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

0
Nicole Calinoiu On

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...

0
Chris Shain On

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) { }
    }
}