logo1

logo

Using Stubs and Shims to Test with Microsoft Fakes in Visual Studio 11

16 Comments
Posted in Unit Testing

Microsoft Fakes is a full featured mocking framework built into Visual Studio 11. Currently, the available documentation is limited and marked as “preview only”, however it does provide us with some very good descriptions.

Microsoft Fakes is an isolation framework for creating delegate-based test stubs and shims in .NET Framework applications. The Fakes framework can be used to shim any .NET method, including non-virtual and static methods in sealed types.

Additionally here is Wikipedia’s definition of Mock Object for reference

In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior of a human in vehicle impacts.

Shim any .NET Method

The real interesting part here is that Microsoft Fakes can shim “any .NET method, including non-virtual and static methods in sealed types”.  Existing mocking frameworks work by providing “on the fly” implementations to preexisting interfaces with a bit of Dependency Injection to get them situated in the class/method under test.

Microsoft Fakes is going a level deeper by mocking objects with no preexisting interfaces and allowing statics to be mocked in the process. This type of mocking is quite complex to pull off.  So much so, that there are only a handful of products out there that do this.  TypeMock’s Isolator, Telerik’s JustMock and the Microsoft Research project Moles that ultimately lead to Microsoft Fakes. 

TypeMock’s Isolator has been a regular so to speak in unit testing circles for a while now and I’ve actively considered it before, but the starting price of $799 has always been a little steep for my tastes. Telerik’s JustMock is the new kid on the block (past 2-3 years). JustMock has freemium edition supporting general mocking features and a pay version which has support for static, sealed & non virtual mocking with a base price of $299.

The biggest surprise is the Microsoft Research project Moles, which seems to have about the same set of features as Microsoft Fakes, has apparently been available since 11/1/2010 and is compatible with both Visual Studio 2010 & 2008 (based on the Visual Studio 2010 Moles x86 - Isolation Framework for .NET download page).  Perhaps this is the internet’s best kept secret… well, maybe not.  Regardless, I’ll be creating a future blog post going over using Moles in Visual Studio 2010.

Architectural Implications

Without a commercial mocking framework or Microsoft Fakes, if you’re going to do unit testing and you want to do it right as in truly isolate the code under test then you’re going to need to mock dependent objects.  However, just mocking the objects isn’t enough, you also need to instruct the code under test to use the mocks as opposed to its normal implementation.  Typically this requires some form of Dependency Injection. 

For reference here is Wikipedia's definition of Dependency Injection:

Dependency injection (DI) is a design pattern in object-oriented computer programming whose purpose is to reduce the coupling between software components. It is similar to the factory method pattern. Frequently an object uses (depends on) work produced by another part of the system. With DI, the object does not need to know in advance about how the other part of the system works. Instead, the programmer provides (injects) the relevant system component in advance along with a contract that it will behave in a certain way.

To truly isolate code in unit tests you’re typically bound to the following practices as part of Dependency Injection:

  • Any object that needs to be mocked needs to be non static and have an interface
  • You’ll either need to have a Dependency Injection framework as part of the architecture or the right patterns in place to manually resolve and inject as needed
  • Constructors will have to be modified to take in interface types for the dependent objects

That’s a lot of Architectural commitment.  As with any design pattern there are “trade offs” involved in usage.  With Dependency Injection you get reduced coupling and the ability to unit test thoroughly, increasing complexity and reducing maintainability by some level in the process.  The question with any design pattern’s usage is: are the trade offs worth it based on the functionality provided for the problem domain in question? 

Microsoft Fakes changes the trade off evaluation for Dependency Injection.  Currently you trade increased complexity and reduced maintainability for increased code quality brought about by unit testing. 

Microsoft Fakes decouples Dependency Injection from unit testing, therefore changing the value proposition for Dependency Injection to instead be weighed on its own merits of whether or not it adds value to the problem domain in question.

Proper unit testing can now be integrated into any codebase, legacy or new, small or large, and using Dependency Injection or not.  With this in mind Microsoft Fakes is a game changer.  Why a game changer now, presumably years after these features have been available in the wild? 

The answer is widespread availability and acceptance.  Anyone running Visual Studio will be able to run this (hopefully this will be part of the bare SDK as well).  Additionally examples, documentation, blogs and training will be much more readily available.  Faking may very well become a recommended practice along with unit testing at which point it could become as pervasive as general unit testing in .NET solutions.  It’s a lot easier to justify its usage when everyone has access to it, not just those willing to shell out $299 to $799 a seat.

Shims vs Stubs

Let’s take a look at the current MSDN definition for Shims and Stubs

Stub types Stub types make it easy to test code that consumes interfaces or non-sealed classes with overridable methods. A stub of the type T provides a default implementation of each virtual member of T, that is, any non-sealed virtual or abstract method, property, or event. The default behavior can be dynamically customized for each member by attaching a delegate to a corresponding property of the stub. A stub is realized by a distinct type which is generated by the Fakes Framework. As a result, all stubs are strongly typed.

Although stub types can be generated for interfaces and non-sealed classes with overridable methods, they cannot be used for static or non-overridable methods. To address these cases, the Fakes Framework also generates shim types.

Shim types Shim types allow detouring of hard-coded dependencies on static or non-overridable methods. A shim of type T can provide an alternative implementation for each non-abstract member of T. The Fakes Framework will redirect method calls to members of T to the alternative shim implementation. The shim types rely on runtime code rewriting that is provided by a custom profiler.

Delegates Both stub types and shim types allow you to use delegates to dynamically customize the behavior of individual stub members.

Stub types appear to be what most free and/or open source mocking frameworks already do for us today, so there’s really no surprises here.  Shim types on the other hand are where the action is at allowing us to detour static or non-overridable methods. 

MSDN mentions that the detouring in shims does degrade performance slightly.  In the brief amount of testing I’ve done there’s about a 20ms difference between shimmed vs stubbed implementation of the same code, which isn’t a deal breaker in terms of keeping tests fast.  It would make sense the cost could be additive based on the amount of detouring you end up doing.  So if you have the infrastructure to do either or, then pick stubbing over shimming. 

Getting Started with Shims

First off, you’ll need Visual Studio 11, fire it up and create a new C# Class Library project called FakingExample.  For this example I’ve put together a very trivial cart example to play with.

Rename the default class file to CartToShim and modify the code to be as follows:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace FakingExample
{
    public class CartToShim
    {
        public int CartId { get; private set; }
        public int UserId { get; private set; }
        private List<CartItem> _cartItems = new List<CartItem>();
        public ReadOnlyCollection<CartItem> CartItems { get; private set; }
        public DateTime CreateDateTime { get; private set; }

        public CartToShim(int cartId, int userId)
        {
            CartId = cartId;
            UserId = userId;
            CreateDateTime = DateTime.Now;
            CartItems = new ReadOnlyCollection<CartItem>(_cartItems);
        }

        public void AddCartItem(int productId)
        {
            var cartItemId = DataAccessLayer.SaveCartItem(CartId, productId);
            _cartItems.Add(new CartItem(cartItemId, productId));
        }
    }
}

Add the CartItem class.

namespace FakingExample
{
    public class CartItem
    {
        public int CartItemId { get; private set; }
        public int ProductId { get; private set; }

        public CartItem(int cartItemId, int productId)
        {
            CartItemId = cartItemId;
            ProductId = productId;
        }
    }
}

Add the DataAccessLayer class. Don’t worry about the connection string, we’ll never end up hitting it anyway.

using System.Data;
using System.Data.SqlClient;

namespace FakingExample
{
    public static class DataAccessLayer
    {
        public static int SaveCartItem(int cartId, int productId)
        {
            using (var conn = new SqlConnection("RandomSqlConnectionString"))
            {
                var cmd = new SqlCommand("InsCartItem", conn);
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.Parameters.AddWithValue("@CartId", cartId);
                cmd.Parameters.AddWithValue("@ProductId", productId);

                conn.Open();
                return (int)cmd.ExecuteScalar();
            }
        }
    }
}

Next add a Unit Test Project called FakingExample.Tests and rename the default unit test class file to CartToShimTests.

In the FakingExample.Tests project add a reference to the FakingExample project.  You’re solution should look as follows:

InitialSolutionConfig

Right click on the FakingExample reference in FakingExample.Tests references and select the “Add Fakes Assembly”.

AddFakesAssembly

This adds a couple of new items to the project.

AfterFakes

The new references add the following types.

NewTypes

The .fakes file created under the Fakes folder turns out to be an xml file.  The file provides a configuration file for the generation of the fakes assembly. I’ll cover more about what can be done in this file later on in the post.  For now here’s a look at the default code generated output.

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/">
  <Assembly Name="FakingExample"/>
</Fakes>

Now we’re ready to do some faking.  Let’s take a look at unit testing the AddCartItem method.  AddCartItem calls SaveCartItem on DataAccessLayer, which happens to be static.  We’re going to mock out that database call to isolate the logic in AddCartItem in our unit test. Add the code below to CartToShimTests.

using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FakingExample.Tests
{
    [TestClass]
    public class CartToShimTests
    {
        [TestMethod]
        public void AddCartItem_GivenCartAndProduct_ThenProductShouldBeAddedToCart()
        {
            //Create a context to scope and cleanup shims
            using (ShimsContext.Create())
            {
                int cartItemId = 42, cartId = 1, userId = 33, productId = 777;

                //Shim SaveCartItem rerouting it to a delegate which 
                //always returns cartItemId
                Fakes.ShimDataAccessLayer.SaveCartItemInt32Int32 = (c, p) => cartItemId;

                var cart = new CartToShim(cartId, userId);
                cart.AddCartItem(productId);

                Assert.AreEqual(cartId, cart.CartItems.Count);
                var cartItem = cart.CartItems[0];
                Assert.AreEqual(cartItemId, cartItem.CartItemId);
                Assert.AreEqual(productId, cartItem.ProductId);
            }
        }
    }
}

Line 13 creates a ShimsContext , which limits the scope of our shimming.  Line 19 defines our shim/detour for the SaveCartItem method.  Notice how we set the new behavior through a property named SaveCartItemInt32Int32. The Int32Int32 on the end is the type signature of parameters accepted by SaveCartItem. Microsoft Fakes has to keep the generated property names unique and predictable for methods, since they could have overloads or be refactored to have them someday.  Now let’s run it in Unit Test Explorer and see if it passes.

TestrunAfterShim

It does indeed pass.  Notice that our shim/detour completely skipped over the SaveCartItem method in DataAccessLayer, if it hadn’t then we would’ve received a nasty exception since “RandomSqlConnectionString” is clearly not a valid connection string . Start to finish, this ends up being pretty easy to setup and use for our simple cart example. 

Stubs Example

For the stubs example our code will be very similar to the shim example however we’ll need to create an ICartSaver interface and inject it into the CartToStub object.  At which point we can stub out ICartSaver and have it return 42 just as we did for shimming.  We’ll manually inject the dependency as opposed to pulling down a DI framework.

Add a new interface ICartSaver to the FakingExample project.

namespace FakingExample
{
    public interface ICartSaver
    {
        int SaveCartItem(int cartId, int productId);
    }
}

Next up add, for completeness (although not necessary) add a CartSaver class to the FakingExample project so we can replicate the implementation of DataAccessLayer.

using System.Data;
using System.Data.SqlClient;

namespace FakingExample
{
    public class CartSaver : ICartSaver
    {
        public int SaveCartItem(int cartId, int productId)
        {
            using (var conn = new SqlConnection("RandomSqlConnectionString"))
            {
                var cmd = new SqlCommand("InsCartItem", conn);
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.Parameters.AddWithValue("@CartId", cartId);
                cmd.Parameters.AddWithValue("@ProductId", productId);

                conn.Open();
                return (int)cmd.ExecuteScalar();
            }
        }
    }
}

Create a new CartToStub class as follows similar to CartToShim but allowing for manual dependency injection of ICartSaver in the constructor:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace FakingExample
{
    public class CartToStub
    {
        public int CartId { get; private set; }
        public int UserId { get; private set; }
        private List<CartItem> _cartItems = new List<CartItem>();
        public ReadOnlyCollection<CartItem> CartItems { get; private set; }
        public DateTime CreateDateTime { get; private set; }
        private ICartSaver _cartSaver;

        public CartToStub(int cartId, int userId, ICartSaver cartSaver)
        {
            CartId = cartId;
            UserId = userId;
            CreateDateTime = DateTime.Now;
            _cartSaver = cartSaver;
            CartItems = new ReadOnlyCollection<CartItem>(_cartItems);
        }

        public void AddCartItem(int productId)
        {
            var cartItemId = _cartSaver.SaveCartItem(CartId, productId);
            _cartItems.Add(new CartItem(cartItemId, productId));
        }
    }
}

Moving back over to the FakingExample.Tests project, add a new unit test file named CartToStubTests.  Create/Modify the AddCartItem_GivenCartAndProduct_ThenProductShouldBeAddedToCart method to take into account the new changes.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FakingExample.Tests
{
    [TestClass]
    public class CartToStubTests
    {
        [TestMethod]
        public void AddCartItem_GivenCartAndProduct_ThenProductShouldBeAddedToCart()
        {
            int cartItemId = 42, cartId = 1, userId = 33, productId = 777;

            //Stub ICartSaver and customize the behavior via a 
            //delegate, ro return cartItemId
            var cartSaver = new Fakes.StubICartSaver();
            cartSaver.SaveCartItemInt32Int32 = (c, p) => cartItemId;

            var cart = new CartToStub(cartId, userId, cartSaver);
            cart.AddCartItem(productId);

            Assert.AreEqual(cartId, cart.CartItems.Count);
            var cartItem = cart.CartItems[0];
            Assert.AreEqual(cartItemId, cartItem.CartItemId);
            Assert.AreEqual(productId, cartItem.ProductId);
        }
    }
}

Run the unit tests once more to see that everything’s passing.

TestrunAfterStub

Just as with shims, start to finish, this ends up being pretty easy overall for our simple cart example.

The Fakes Xml File

The current MSDN library documentation details the nature of the .fakes xml file as follows:

The generation of stub types is configured in an XML file that has the .fakes file extension. The Fakes framework integrates in the build process through custom MSBuild tasks and detects those files at build time. The Fakes code generator compiles the stub types into an assembly and adds the reference to the project.

The .fakes file provides fine grained control of how stub and shim generation work for a particular assembly.  Luckily enough the file has intellisense and pretty good descriptions when hovering, so exploration of features is rather straight forward.  Expanding out all attributes and elements using intellisense provides us with the following structures.

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/">
  <Assembly Name="" Location="" Version="" x86=""/>

  <StubGeneration Disable="" SkipVirtualIndexers="" SkipVirtualMethods="">
    <Clear />
    <Add AbstractClasses="" FullName="" Interfaces="" Namespace="" TypeName=""/>
    <Remove AbstractClasses="" FullName="" Interfaces="" Namespace="" Obsolete="" TypeName=""/>
  </StubGeneration>

  <ShimGeneration Disable="">
    <Clear />
    <Add FullName="" Namespace="" TypeName=""/>
    <Remove FullName="" Namespace="" Obsolete="" TypeName=""/>
  </ShimGeneration>

  <Compilation Debug="" DisableCodeContracts="" KeyFile="" ProjectTemplate="">
    <COMReference Guid="" VersionMajor="" VersionMinor="" WrapperTool=""/>
    <Property Condition="" Name="" />
  </Compilation>
</Fakes>

Based on this we see the following features:

  • Either shimming or stubbing can be completely turned off
  • Shims and stubs can be filtered such that only specific items from an assembly are shimmed and/or stubbed
  • Strong signing can be overridden via the KeyFile attribute, based on the MSDN docs the Fakes framework will automatically sign the .Fakes assembly with the same key the source assembly was signed with unless overridden here
  • There’s a facility to reference/deal with a COM component with COMReference if necessary.  Hopefully it won’t be…

Aside from items within the compilation element everything is along the lines of what we’d expect to be available here.  I imagine in most circumstances this file won’t need to be touched, however if you’re faking a larger library like mscorlib, then it would seem almost mandatory to filter the types generated otherwise the compiler could be chewing on it for a while.  The classic DateTime.Now shimming examples should probably be doing this type of filtering.

Conclusion

All in all I’m really excited about Microsoft Fakes.  It has the capacity to drive change in the .NET testing landscape similarly to how adding unit testing to Visual Studio (back in the 2005 version I believe) did so.  Widespread inclusion, usage and education of faking/mocking will be very beneficial for the community and will help drive innovation of testing techniques. Projects which would’ve been considered difficult if not impossible to add some level of unit testing to, without large amounts of refactoring, can now be unit tested easily.  Microsoft Fakes is a definitive win in my book.

 

The code for this post is available on GitHub

  • 16 Comments

Comments (16) -

RyanonRails

Hey Rich,

Great article with some "real world" examples. Fakes is so new that the documentation isn't fully fleshed out yet (although I guess vs11 is still in beta). Only a few blog posts out there. This is probably the best so far.

Thanks,
Ry

Rich Czyzewski

Thanks Ryan.  I'm glad you liked it.  I was getting tired of all the DateTime and File IO examples out there.  It'll be pretty interesting to see where the development/testing community takes this and how "baking in" mocking features will evolve generally accepted testing practices in .NET.

Christian Jacob

You're absolutely right! I stumbled across Fakes when there was no single reference about it in the internet except for the mentioning on the moles website on Microsoft Research and the 4 MSDN Help-Pages. I was thrilled to see that it was integrated into Visual Studio 11.

File IO and DateTime seemed to be the perfect entry for people to see what mocking - and specifically Fakes - was all about. So my first blogpost on 3/21 showed exactly that. I also prepared a quickhit video for the german Visual Studio 11 beta roadshow. In about 10 minutes at max there is not much time showing much more than than the usual suspects.

However, I plan to release a series about fakes showing more real world samples. And I have to agree on Ryan: Up to now, your post is far the best and detailed description about Fakes. Well done!

Toni

I've been using many different mocking frameworks and this looks so complicated. I mean if you take FakeItEasy, NSubstitute or Moq it is simple and it just works. There is no magic, no xml files or anything like that.

Rich Czyzewski

Toni,

I agree that there is some extra complexity compared to current mocking frameworks, but only by a small amount. With the xml, I'd expect the user not to be working with it much if at all. At that point the only extra thing a user has to deal with is right clicking on "Add Fakes Assembly". From there it's just syntax differences, which although not the most fluent, do get the job done.

FakeItEasy, NSubstitute and Moq all use a dynamic proxy under the covers and only address the stubbing portion of what Microsoft Fakes has provided. I've looked through the Moles documention (research.microsoft.com/en-us/projects/pex/molesmanual.docx), the precursor to Fakes, to get an idea of why dynamic proxies weren't used.

The best I can guess why they chose assembly generation over dynamics is performance and integration with their dynamic whitebox test-generation tool Pex. So perhaps there's more to come with Pex integration or they may make further improvements by RTM.

Despite shortcomings versus other tools, I would still argue that including this functionality in Visual Studio is a step forward in popularizing testing techniques.

-Rich




Neil Nordhaus

Great article.  I have one major concern though. Will MS Fakes only be available within Visual Studio?  Not being able to run it via the command line the same way I can with MSBuild and MSDeploy on my build server is a deal breaker for my project.

Rich Czyzewski

Thanks Neil.

Fakes runs as part of the test host itself, meaning as long as you can run a MSTest test or a 3rd party runner that invokes the host process you should be fine command line wise.

Unfortunately, if we're strictly speaking MSTest here, to get it on the build server has traditionally been a headache. You ultimately need to install some VS sku (minimally Visual Studio Test Agent) on the build server itself to get it to work (I'm not sure if they plan to change this in VS11).  See http://stackoverflow.com/a/9162464/826136 for more info.

Additionally I'd be surprised if 3rd party runners like NUnit don't end up supporting this functionality out of the box.

Ashutosh Trasi

Great article, Rich! I understand that using Shims will allow you to mock behavior of static classes and methods. But wouldn't that actually diminish the mentality that Static classes and methods are hard to test. One of the great things about IOC and DI is to, not only provide loose coupling and unit-testable code, but also to steer developers away from writing code that is hard to test. With the provision of Shims, I feel like developers wouldn't be as bothered if they found themselves writing static classes and methods. Would like to hear your thoughts.

Rich Czyzewski

Thanks Ashutosh,

Microsoft Fakes is a game changer in terms of changing the definition of what unit testable code means. Since Fakes is capable of testing anything, then all code, no matter the design, is inheritly testable and easily testable at that. So, I agree that it will change the mentality of what is considered hard to test. That's a lot to think about for a lot of people.

In the future, perhaps better general guidance will be required to steer developers in the right directions, since being unit testable will have an entirely new meaning with the introduction of Fakes.

I'm not exactly sure where we, as a testing community, will end up after Fakes becomes widely available, however as an architect I'll enjoy the flexibility it affords me to test anything under the sun.

Rich

David

This is a helpful post. Has anyone been able to get these fakes to work with WinRT (metro) components? When I select 'Add Fakes Assembly', I get an error "code: assembly <dir>\<assemblyName> failed to load properly" I have tried 'Window Metro Style App', 'WinMD File', and 'Class Library' and get the same behavior.  

Rich Czyzewski

David,

Oddly enough, it looks like Windows RT/Metro components aren't supported by Microsoft Fakes yet. See this forum post for more info social.msdn.microsoft.com/.../6881dfda-41af-44a1-98de-21f04d94dc5b

Unfortunately Fakes does not work right now with Metro-style managed apps. It is something we're interested in for the future, but it is not available right now.
--
Peter Provost


And it looks like they have no estimate of when it will be in there. I'd be one to hope they have it by RTM.

Rich

Jignesh

When I add fake assembly reference, it fails with following:

Error  8  'XYZ.Data.Object.Fakes.StubApprovalRequest.AID': cannot override because 'XYZ.Data.Object.ApprovalRequest.AID' is not a property

and my class definition is as below:

public class ApprovalRequest
{
        public Guid AID;
        public Guid CreatedBy;
        :
        :
}

AID is a public member variable.

Do you have any comments how this can be resolved?

Rich Czyzewski

Jignesh,

It looks like you're trying to detour a member field. Shims as part of Microsoft Fakes works by detouring methods. This works with properties since under the covers properties are implemented as methods by the compiler. The MSDN summary for shims: msdn.microsoft.com/en-us/library/hh549176(v=vs.110) describes their behavior as follows:

Shim types are one of two technologies that the Microsoft Fakes Framework uses to let you easily isolate unit tests from the environment. Shim types allow detouring of hard-coded dependencies on static or non-overridable methods.

So it looks like you'll have to refactor AID to be a property in order to shim it.

Rich

Sandeep Kumar

@Jignesh: Try setting the target framework of your test project to the one that your product project is targeted to. This blog I found on internet might help:-
msmvps.com/.../...rary-to-fake-out-sharepoint.aspx

Nathan

I just saw a demonstration of this at TechEd by Peter Provost from Microsoft.  He said that this is only a Visual Studio Ultimate feature.  So definitely not available for everyone.

Right now I have a Visual Studio MSDN Pro subscription, so this will cost me on the order of $10k to obtain this feature (the difference between the pro and ultimate subscription).

Rich Czyzewski

Nathan,

Hmmm... looks like its availability has been changed. Originally both MSDN and Peter detailed that it was available to the Premium and Ultimate skus. Microsoft Connect has an issue opened that confirms it'll only be available in Ultimate:
connect.microsoft.com/.../fakes-framework-is-not-available-in-vs12-premium

Just to clarify for you. The documentation link is an error and will be corrected. Fakes will ship only in Visual Studio Ultimate.

Thanks,
Joshua Weber


Originally I was going off of this MSDN article: msdn.microsoft.com/.../dd264975%28v=vs.110%29.aspx

The Microsoft Fakes isolation framework is available on in Visual Studio Ultimate and Visual Studio Premium.

And comments made by Peter back in March (quoted below) on blogs.msdn.com/.../...io-11-beta-unit-testing.aspx

The Fakes framework is available in the Premium and Ultimate VS11 SKUs.

I agree, being only in Ultimate does significantly limit availability and I wouldn't pay 10k just for this feature as you can get it from Typemock or Telerik for much cheaper. I'll update the post when VS2012 goes RTM, just in case they change their mind again ;)

Rich

Pingbacks and trackbacks (2)+

Comments are closed