Monday, September 19, 2016

Are you addressing a pain or you just polka polka?!




I was watching Home Alone, one of my favourite classic movies, with my 8yro last week. I watched the movie several times before, but this time, a scene stopped me thinking. The scene was capturing a desperate mother trying to get to back her son who is home alone. She was at some airport trying to get any seat on any flight to get back to Chicgo. 

The airport staff didn’t help her much and then a guy appeared out of nowhere and offered to help her. 


The conversation went like:

Mother (angry): (Addressing airport staff) ...if I have to sell my soul to the Devil himself...

...I am going to get home to my son.

Airport Staff: Ma'am, if there was anything...

Mother: Do it. Do anything.

Airport Staff: - I can get you a hotel room. - What?

Polka guy: Can you excuse us for a sec? Can I see you for a second, please?

Excuse us.

You got a little bit of a dilemma.

We got a crisis ourselves.

Allow me to introduce myself. Gus Polinski.

Polka King of the Midwest? (The mother looks puzzled at him)

The Kenosha Kickers?

That's okay. I thought you might have recognized...

I had a few hits a few years ago. That's why I just...

"Polka, Polka, Polka"? Polka, polka, polka

"Yamahoozie Polka," a.k.a. "Kiss Me Polka"? "Polka Twist"?

Mother: These are songs?

Polka guy: Yeah. Yeah, we... Some fairly big hits for us.

You know, in the early '70s.

Yeah, we sold about 623 copies of that.

Mother: - In Chicago?
Polka guy: No, Sheboygan. Very big in Sheboygan.

Mother: Did you say you could help?

Polka guy: Anyway, I'm rambling on here. Our flight was canceled...

...so we're gonna drive. See the guy in the yellow jacket over there?

He's gonna rent us a nice big van to drive to Milwaukee.

Now, I heard you had some problems getting to Chicago?

To see your kid or something?

Mother: Uh, my son. He... We left, and he's there.

Polka guy: If you have to get to Chicago, we'll gladly drive you.

Mother: - You'd give me a ride? - Sure, why not?


Mr. Polinski, in the scene, did sincerely want to help the lady, but it took him too many introductory statements!

In some cases, I see consultants try to solve a problem that is not addressing a real pain, but rather, something that they thought is the problem. When discussing a solution with a client, I always think, “What problem/pain am I addressing here?”. If I’m not adding a business value or fixing a real problem, then I just polka polka!

Saturday, October 5, 2013

Step-by-step Guide - MVC 4 + Entity Framework 6 + Autofac + Generic Repository Pattern

This post is a step-by-step guide to laying out a web app based on MVC4 and utilises autofac for IoC EF6 for data access, and implements the generic repository pattern.

I know there might be some other complete examples similar to what I am writing here, but I thought I can write this as a step-by-step, explaining every step of the way.

Background

Developer: I want to build A typical MVC4 web app that connects resiliently to a data store. This web app should be built using IoC and implements the Generic Repository Pattern.

Ingredients
  1. Visual Studio 2012/2013 (any edition)
  2. IIS Express (or IIS)
  3. Sql Express (or Sql Server)
  4. Internet connection :)
Recipe

Step 1 - MVC Web App

In this step, create a simple MVC 4 Web App by doing the following:
  1. Open Visual studio
  2. Create a new Web - MVC4 Web App project
  3. Choose Empty Template

Step 2 - Install Autofac nuget package

To use Autofac for dependency Injection,

  1. Open Package Manager (from Tools menu)
  2. Search for Autofac.MVC4
  3. Install it
  4. Search for EntityFramework, and make sure you have included Prereleases.

Alternatively, you can run the following command from inside Package Manager Console

Install-Package Autofac.MVC4

Step 3 - Create DB Model

In this step, we will create a database model to represent data structure in the database.

  1. Add a new class library project to the solution
  2. Delete class1.cs from the project!
  3. Create two new folders to the project namely, DatabaseModel and DataAccess. These folders will hold the database model and data access classes respectively.
  4. right-click the first folder (DatabaseModel) and add new item
  5. Choose ADO.NET Entity Data Model, and type a good name for it.
  6. Choose "Generate from Database" in Model contents step, click Next
  7. When prompted to choose EF version, choose EF 6.0
  8. Select  a couple of Tables from the database. (In this sample, I'm using AdventureWorks2012 database .. you can download it from this link)

Step 4 - Implement Generic Repository

Now that we created our database model, we'll create data access class that implements the generic repository pattern.
  1. IDbContext: This interface will abstract all DBSets inside the context class generated in step3, and also adding a SaveChanges method which will be further used by Generic Repo to submit changes to the database

    using System.Data.Entity;
    using System.Data.Entity.Core.Objects;
    using System.Data.Entity.Infrastructure;
    
    namespace Blog.Samples.MVCEF6.Data.DataAcess
    {
        public interface IDbContext
        {
            IDbSet<tentity> Set<tentity>() where TEntity : class;
            int SaveChanges();
        }
    
        public static class DbContextExtensions
        {
            public static ObjectContext GetObjectContext(this IDbContext dbContext)
            {
                return ((IObjectContextAdapter)dbContext).ObjectContext;
            }
        }
    }
  2. Change context t4 template to reference IDbConext. You should find this file under the database model, namely [Something].Context.tt ..
    From
    <#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext
    

    To
    <#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext, IDbContext
    


    And add the following lines
    #>
        public ObjectContext ObjectContext
        {
            get { return this.GetObjectContext(); }
        }
    
        public new IDbSet<tentity> Set<tentity>() where TEntity : class
        {
            return base.Set<tentity>();
        }
    
        public new int SaveChanges()
        {
            return base.SaveChanges();
        }
        <#
    


    right after these lines
    <#
        foreach (var entitySet in container.BaseEntitySets.OfType())
        {
    #>
        <#=codeStringGenerator.DbSet(entitySet)#>
    <#
        }
    


    Also the Using directives
    using System.Data.Entity.Core.Objects;
    using Blog.Samples.MVCEF6.Data.DataAcess;
    


    right after
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure;
    
  3. Rebuild the project
  4. IObjectContext: This interface abstracts the ObjectContext (which in turn encapsulates the interaction between the database and the CLR.
    using System;
    using System.Data.Entity.Core.Objects;
    
    namespace Blog.Samples.MVCEF6.Data.DataAcess
    {
        public interface IObjectContext : IDisposable
        {
            void SaveChanges();
            ObjectContextOptions ContextOptions { get; }
        }
    }
    
    
  5. IObjectSetFactory: factory pattern implementation over ObjectSets
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Core.Objects;
    
    namespace Blog.Samples.MVCEF6.Data.DataAcess
    {
        public interface IObjectSetFactory : IDisposable
        {
            IObjectSet<T> CreateObjectSet<T>() where T : class;
            void ChangeObjectState(object entity, EntityState state);
        }
    }
    
    
  6. ContextAdaptor: an Implementation of IObjectSetFactory and IObjectContext
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Core.Objects;
    
    namespace Blog.Samples.MVCEF6.Data.DataAcess
    {
        public class ContextAdaptor : IObjectSetFactory, IObjectContext
        {
            private readonly ObjectContext _context;
    
            public ContextAdaptor(IDbContext context)
            {
                _context = context.GetObjectContext();
            }
    
            public void SaveChanges()
            {
                _context.SaveChanges();
            }
    
            public ObjectContextOptions ContextOptions
            {
                get { return _context.ContextOptions; }
            }
    
            public IObjectSet<T> CreateObjectSet<T>() where T : class
            {
                return _context.CreateObjectSet<T>();
            }
    
            public void ChangeObjectState(object entity, EntityState state)
            {
                _context.ObjectStateManager.ChangeObjectState(entity, state);
            }
    
            private bool _disposed;
            protected virtual void Dispose(bool disposing)
            {
                if (!_disposed)
                {
                    if (disposing && _context != null)
                    {
                        _context.Dispose();
                    }
                }
                _disposed = true;
            }
    
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
        }
    }
  7. IUnitOfWork and its implementation: this will control when/how to commit CLR changes to database
    using System;
    
    namespace Blog.Samples.MVCEF6.Data.DataAcess
    {
        public interface IUnitOfWork : IDisposable
        {
            void Commit();
            bool LazyLoadingEnabled { set; get; }
        }
    }
    

    and its implementation
    using System;
    
    namespace Blog.Samples.MVCEF6.Data.DataAcess
    {
        public class UnitOfWork : IUnitOfWork
        {
            private readonly IObjectContext _objectContext;
    
            public UnitOfWork(IObjectContext objectContext)
            {
                _objectContext = objectContext;
            }
    
            public void Commit()
            {
                _objectContext.SaveChanges();
            }
    
            public bool LazyLoadingEnabled
            {
                set { _objectContext.ContextOptions.LazyLoadingEnabled = value; }
                get { return _objectContext.ContextOptions.LazyLoadingEnabled; }
            }
    
            private bool _disposed;
            protected virtual void Dispose(bool disposing)
            {
                if (!_disposed)
                {
                    if (disposing && _objectContext != null)
                    {
                        _objectContext.Dispose();
                    }
                }
                _disposed = true;
            }
    
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
        }
    }
    
  8. IRepository: This interface abstracts all Repository-related operations, such as Get, Delete, Count, etc...
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace Blog.Samples.MVCEF6.Data.DataAcess
    {
        public interface IRepository<TEntity> : IQueryable
            where TEntity : class
        {
            int CountAll(params Expression<Func<TEntity, object>>[] includeProperties);
            int Count(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties);
            IEnumerable<TEntity> GetAll(params Expression<Func<TEntity, object>>[] includeProperties);
            IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties);
            TEntity Single(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties);
            TEntity SingleOrDefault(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties);
            TEntity First(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties);
            TEntity FirstOrDefault(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties);
            void Delete(TEntity entity);
            void Insert(TEntity entity);
            void Update(TEntity entity);
            void SaveChanges();
        }
    }
    

  9. Repository'T: Implementation of IRepository interface
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Core.Objects;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace Blog.Samples.MVCEF6.Data.DataAcess
    {
        public class Repository<TEntity> : IRepository<TEntity>, IQueryable
            where TEntity : class
        {
            private readonly IObjectSet<TEntity> _objectSet;
            private readonly IObjectSetFactory _objectSetFactory;
            private readonly IUnitOfWork _unitOfWork;
    
            public Repository(IObjectSetFactory objectSetFactory, IUnitOfWork unitOfWork)
            {
                _objectSet = objectSetFactory.CreateObjectSet<TEntity>();
                _objectSetFactory = objectSetFactory;
                _unitOfWork = unitOfWork;
            }
    
            public virtual IQueryable<TEntity> AsQueryable()
            {
                return _objectSet;
            }
    
            public virtual int CountAll(params Expression<Func<TEntity, object>>[] includeProperties)
            {
                IQueryable<TEntity> query = AsQueryable();
                query = PerformInclusions(includeProperties, query);
                return query.Count();
            }
    
            public virtual int Count(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties)
            {
                IQueryable<TEntity> query = AsQueryable();
                query = PerformInclusions(includeProperties, query);
                return query.Count(where);
            }
    
            public virtual IEnumerable<TEntity> GetAll(params Expression<Func<TEntity, object>>[] includeProperties)
            {
                IQueryable<TEntity> query = AsQueryable();
                query = PerformInclusions(includeProperties, query);
                return query.ToList();
            }
    
            public virtual IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> where,
                                       params Expression<Func<TEntity, object>>[] includeProperties)
            {
                IQueryable<TEntity> query = AsQueryable();
                query = PerformInclusions(includeProperties, query);
                return query.Where(where);
            }
    
            public virtual TEntity Single(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties)
            {
                IQueryable<TEntity> query = AsQueryable();
                query = PerformInclusions(includeProperties, query);
                return query.Single(where);
            }
    
            public virtual TEntity First(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties)
            {
                IQueryable<TEntity> query = AsQueryable();
                query = PerformInclusions(includeProperties, query);
                return query.First(where);
            }
    
            public virtual TEntity SingleOrDefault(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties)
            {
                IQueryable<TEntity> query = AsQueryable();
                query = PerformInclusions(includeProperties, query);
                return query.SingleOrDefault(where);
            }
    
            public virtual void Delete(TEntity entity)
            {
                _objectSet.DeleteObject(entity);
            }
    
            public virtual void Insert(TEntity entity)
            {
                _objectSet.AddObject(entity);
            }
    
            public virtual void Update(TEntity entity)
            {
                _objectSet.Attach(entity);
                _objectSetFactory.ChangeObjectState(entity, EntityState.Modified);
            }
    
            private static IQueryable<TEntity> PerformInclusions(IEnumerable<Expression<Func<TEntity, object>>> includeProperties,
                                                           IQueryable<TEntity> query)
            {
                return includeProperties.Aggregate(query, (current, includeProperty) => current.Include(includeProperty));
            }
    
    
            public void SaveChanges()
            {
                _unitOfWork.Commit();
            }
    
            public TEntity FirstOrDefault(Expression<Func<TEntity, bool>> where, params Expression<Func<TEntity, object>>[] includeProperties)
            {
                var query = AsQueryable();
                query = PerformInclusions(includeProperties, query);
                return query.FirstOrDefault(where);
            }
    
            public IEnumerator GetEnumerator()
            {
                return _objectSet.GetEnumerator();
            }
    
            public Expression Expression
            {
                get { return _objectSet.Expression; }
            }
            public Type ElementType
            {
                get { return _objectSet.ElementType; }
            }
            public IQueryProvider Provider
            {
                get { return _objectSet.Provider; }
            }
        }
    }
    
    
Your Data project should look similar to this:

Step 5 - Implement Domain Assembly

So, we got the database model, and data access classes. Now, we are going to implement some business (domain) services that should consume database and perform some logic on it. This assembly will serve the WebApp (or whatever app) that we created in Step1

  1. Add a new class library project to the solution
    step 5 first pic goes here
  2. Name it something awesome
  3. Delete class1.cs :)
  4. Add reference to the Data project
  5. Beef up the assembly by adding a sample service that uses data access classes to retrieve a list of models
    • right-click the domain project
    • add a new folder with the name of 'Services' (arbitrarily)
    • Add a new interface under this folder, name it IProductsServices:
      using System.Collections.Generic;
      using Blog.Samples.MVCEF6.Data.DatabaseModel;
      
      
      namespace Blog.Samples.MVCEF6.Domain.Services
      {
          public interface IProductsServices
          {
              IEnumerable<product> ListProductsByVendor(int businessEntityID);
              IEnumerable<vendor> ListVendors();
          }
      }
      
    • Implement the interface by consuming the Repository'T
      using System.Collections.Generic;
      using System.Linq;
      using Blog.Samples.MVCEF6.Data.DataAcess;
      using Blog.Samples.MVCEF6.Data.DatabaseModel;
      
      namespace Blog.Samples.MVCEF6.Domain.Services
      {
          public class ProductsServices : IProductsServices
          {
              private readonly IRepository<Product> _productsRepository;
              private readonly IRepository<Vendor> _vendorsRepository;
              private readonly IRepository<ProductVendor> _productsVendorsRepository;
      
              public ProductsServices(IRepository<Product> productsRepository,
                  IRepository<Vendor> vendorsRepository,
                  IRepository<ProductVendor> productsVendorsRepository)
              {
                  _productsRepository = productsRepository;
                  _vendorsRepository = vendorsRepository;
                  _productsVendorsRepository = productsVendorsRepository;
              }
      
              public IEnumerable<Product> ListProductsByVendor(int businessEntityID)
              {
                  return _productsRepository.Find(a => a.ProductVendors.Any(pv => pv.BusinessEntityID == businessEntityID));
              }
      
              public IEnumerable<Vendor> ListVendors()
              {
                  return _vendorsRepository.GetAll();
              }
          }
      }
      

Step 6 - Wire up using Autofac

We've got the database model and the Data access classes and domain assembly, the next step is to wire all this up with the web application using Autofac.
  1. In the web app project, Add reference to the domain project that we created in Step 5,
  2. Add DependencyConfig class: This class contains any IoC-related code, We're going to call its main function from Global.asax.cs
    • right-click the App_start folder in the web project, and add a new class, name it "DependencyConfig"
    • Add the following code to the class:
      using Autofac;
      using Autofac.Integration.Mvc;
      using Blog.Samples.MVCEF6.Data.DataAcess;
      using Blog.Samples.MVCEF6.Data.DatabaseModel;
      using System.Web.Mvc;
      
      namespace Blog.Samples.MVCEF6.Web.App_Start
      {
          public class DependencyConfig
          {
              public static void Configure(ContainerBuilder builder)
              {
                  builder.RegisterControllers(typeof(MvcApplication).Assembly)
                         .PropertiesAutowired();
      
                  builder.RegisterAssemblyTypes(typeof(ContextAdaptor).Assembly)
                      .AsImplementedInterfaces()
                      .InstancePerHttpRequest();
      
                  builder.RegisterType<AdventureWorks2012Entities>()
                      .As<IDbContext>()
                      .InstancePerHttpRequest();
      
                  builder.RegisterType<ContextAdaptor>()
                      .As<IObjectSetFactory, IObjectContext>()
                      .InstancePerHttpRequest();
      
                  builder.RegisterType<UnitOfWork>()
                      .As<IUnitOfWork>()
                      .InstancePerHttpRequest();
      
                  builder.RegisterGeneric(typeof(Repository<>))
                      .As(typeof(IRepository<>))
                      .InstancePerHttpRequest();
      
                  var container = builder.Build();
                  var dependencyResolver = new AutofacDependencyResolver(container);
                  DependencyResolver.SetResolver(dependencyResolver);
              }
          }
      }
  3. Call DepedencyConfig.Configure from global.asax.cs
    • add the following line inside Aplication_start function, right after RegisterAllAreas line
      DependencyConfig.Configure(new ContainerBuilder());

Step 7 - Add some Controllers/Views to test it out

Let's now add some MVC stuff in the web project:
  1. right-click Models folder, add a new class to represent the HomeViewModel:
    using Blog.Samples.MVCEF6.Data.DatabaseModel;
    using System.Collections.Generic;
    
    namespace Blog.Samples.MVCEF6.Web.Models
    {
        public class HomeViewModel
        {
            public IEnumerable<Vendor> Vendors { get; set; }
            public int SelectedBusinessEntityID { get; set; }
        }
    }
    
    
  2. right-click Controllers folder, and a new HomeController, Use the following code to beef it up.
    using System.Web.Mvc;
    using Blog.Samples.MVCEF6.Domain.Services;
    using Blog.Samples.MVCEF6.Web.Models;
    
    namespace Blog.Samples.MVCEF6.Web.Controllers
    {
        public class HomeController : Controller
        {
            private readonly IProductsServices _productsServices;
    
            public HomeController(IProductsServices productsServices)
            {
                _productsServices = productsServices;
            }
    
            public ActionResult Index()
            {
                return View(new HomeViewModel()
                    {
                        Vendors = _productsServices.ListVendors()
                    });
            }
    
        }
    }
    

  3. right-click Views folder, and a new folder, name it "Home" ..right-click Home folder under Views, add an Index view. You can reference _Layout.cshtml as the master page. Use the following code to beef it up.
    @model Blog.Samples.MVCEF6.Web.Models.HomeViewModel
    @{
        ViewBag.Title = "Index";
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
    
    <h2>Index</h2>
    
    @Html.DropDownListFor(m => m.SelectedBusinessEntityID, Model.Vendors.Select(v => new SelectListItem
        {
            Text = v.Name,
            Value = v.BusinessEntityID.ToString()
        }))
    
    
  4. I won't be consuming the GetProductByVendor in this sample, I'm leaving this for you as an exercise.

You can find the complete code here

Thursday, September 26, 2013

Load testing your cloud app - Tools comparison

If you’re planning to load test your cloud app, the comparison below might be handy.
The research I have done is on these service providers:
  1.          Soasta CloudTest,
  2.          Blitz,
  3.          BlazeMeter,
  4.          LoadStorm,
  5.          and MicrosoftCloud-based Load test tool.


Soasta – CloudTest

Pros

  •          A complete environment to setup tests
  •          Add test users locations seamlessly
  •          a good coverage around the globe
  •          A comprehensive real-time interface to metrics
  •          Drill down analysis on web requests
  •          A Lite (Free) version is available with a limit of 100 concurrent users

Cons

  •          Requires some setup: you have to spin up a VM to load the test environment

Pricing

“CloudTest is sold based on server hours, including support. With no limits on the number of testers or software access, you select
Plans based on how many test server hours you’ll run. Plans are available for coverage of a few tests each month up to thousands of tests each year” - http://www.soasta.com/wp/wp-content/uploads/2012/10/DS-CloudTest-100812.pdf

Blitz

Pros

  •          Capacity planning tools, Optimization, Performance monitoring are out-of-the-box
  •          Nothing to install/setup, all cloud-based
  •          Simulate up to 5,000 concurrent virtual users
  •          It has a chrome/firefox plugin to monitor app performance on develop environment
  •          Integrated with Google Analytics
  •          Performance Monitoring Alerts  (Email, Twitter, SMS, PagerDuty)

Cons

  •          Only 8 test locations around the globe (Australia, Brazil, California Ireland, Japan, Oregon, Singapore, Virginia)
  •          Relatively less sophisticated interface (compared to Soasta)

Pricing

  •          Works with “Credits”
  •          1 credit = 1 minute and 1,000 users - https://www.blitz.io/pricing
  •          40 Credits = $40
  •          150 Credits = $135
  •          300 Credits = $240

BlazeMeter

Pros

  •          JMeter compliant
  •          Easy to use test creation interface
  •          Nice interface for test results
  •          You can export test results
  •          Compare two test runs
  •          Has a server in Australia (yes, we’re load-testing apps in Oz)
  •          Test scheduling
  •          Integrated with Google Analytics
  •          VPN is supported
  •          Chrome extension to record JMeter scripts and upload it to BlazeMeter
  •          It has a free version that can simulate up to 50 concurrent users
  •          A good customer base

Cons

  •          Limited Geo coverage (California, Oregon and Virginia USA, Tokyo, Ireland, Brazil, Singapore and Australia)

Pricing

  •          Monthly plans – Starting $199 (1,000 users, 20 hours) up to “Call Us”
  •          On-Demand (Per hour) – Starting $19 (1,000 users, 1 server) to $299 (40,000 users, 40 servers)
  •          http://blazemeter.com/pricing



LoadStorm

Pros

  •          Scalable up to 300,000 users
  •          A testing data centre in Sydney, Australia
  •          Supports automatic crawling (Spider)
  •          No scripting language is involved
  •          Nothing to install

Cons

  •          Currently they only cover Virginia, California, Oregon, Ireland, Sydney, Tokyo, Singapore, Sao Paulo
  •          No built-in feature to collect server performance metrics

Pricing

  •          Monthly plans – Starting Free (100 users, 25 users per test) up to “Call Us”
  •          On-Demand (Per hour) – Starting $39 (1,000 users per test) to “Call” (10,000+ users)
  •          http://loadstorm.com/load-testing-cost/



Microsoft Cloud-based Load test tool

Pros

  •          MS offers a free version of TFS services (up to 5 developers)
  •          Reuse on-premise tests
  •          Easy to set-up

Cons

  •          Requires Visual Studio™ Ultimate (Pricy)

Pricing

  •          It’s still vague - varies according to your MSDN subscription
  •          If you don’t have an MSDN subscription, you’ll have to buy VS Ultimate (around AU$19,274) and then subscribe for TFS Services