views:

481

answers:

2

My web app has some slight variations in business logic and presentation logic depending on the type of user that is logged in. It seems like getting variations by injecting different concrete classes based on the user type is a good fit for DI. So I'm wondering what features of StructureMap I should use to achieve this (or if I'm way off base on the purposes of DI).

(I just learned that Profiles are not the way to accomplish this because setting the Profile isn't a per-thread operation: http://stackoverflow.com/questions/1494937/are-structuremap-profiles-thread-safe)

EDIT

Is this the way to go about this?

public class HomeController
{
    private ISomeDependancy _someDependancy;

    public HomeController(ISomeDependancy someDependancy)
    {
        _someDependancy = someDependancy;
    }

    public string GetNameFromDependancy()
    {
        return _someDependancy.GetName();
    }
}

public interface ISomeDependancy
{
    string GetName();
}

public class VersionASomeDependancy : ISomeDependancy
{
    public string GetName()
    {
        return "My Name is Version A";
    }
}

public class VersionBSomeDependancy : ISomeDependancy
{
    public string GetName()
    {
        return "My Name is Version B";
    }
}

public class VersionARegistry : Registry
{
    public VersionARegistry()
    {
        // build up complex graph here
        ForRequestedType<ISomeDependancy>().TheDefaultIsConcreteType<VersionASomeDependancy>();
    }
}

public class VersionBRegistry : Registry
{
    public VersionBRegistry()
    {
        // build up complex graph here
        ForRequestedType<ISomeDependancy>().TheDefaultIsConcreteType<VersionBSomeDependancy>();
    }
}

public class ContainerA : Container
{
    public ContainerA()
        : base(new VersionARegistry())
    {
    }
}

public class ContainerB : Container
{
    public ContainerB()
        : base(new VersionBRegistry())
    {
    }
}

[TestFixture]
public class Harness
{
    [Test]
    public void ensure_that_versions_load_based_on_named_containers()
    {
        ObjectFactory.Initialize(c =>
        {
            c.ForRequestedType<IContainer>().AddInstances(
                x =>
                {
                    x.OfConcreteType<ContainerA>().WithName("VersionA");
                    x.OfConcreteType<ContainerB>().WithName("VersionB");
                }).CacheBy(InstanceScope.Singleton);
        });

        HomeController controller;

        IContainer containerForVersionA = ObjectFactory.GetNamedInstance<IContainer>("VersionA");
        controller = containerForVersionA.GetInstance<HomeController>();
        Assert.That(controller.GetNameFromDependancy(), Is.EqualTo("My Name is Version A"));

        IContainer containerForVersionB = ObjectFactory.GetNamedInstance<IContainer>("VersionB");
        controller = containerForVersionB.GetInstance<HomeController>();
        Assert.That(controller.GetNameFromDependancy(), Is.EqualTo("My Name is Version B"));
    }
}
+2  A: 

I would say that this is not the core purpose of DI - that is to wire up and inject dependencies, whatever they might be. No application logic should be involved in the wiring up of components - it should be based stricly on configuration; either by code or .config file. That configuration is application-wide, so it's quite difficult to define a configuration that varies by user.

That said, what you are asking about goes well hand in hand with DI - it's just a bit perpendicular to DI in itself.

For your specific purpose, I would define a new dependency in the form of an interface or abstract base class. This would be a Strategy that selects the correct concrete types (the ones you want to vary) based on the current user.

You can use DI to inject all the available concrete types into this Strategy, which would then have a method or property that returns the correct choice among those injected services, based on the current user.

In all the places where you used to depend on the vary-by-user-services, you remove those old dependencies and replace them with a dependency on the Strategy.

Mark Seemann
+1 for giving what I'm trying to do the correct name (I changed the question's title to reflect). But you essentially just restated what I'm trying to accomplish. I would like to know what features of StructureMap work best to implement the "Strategy" pattern. Or, if DI is not meant to do such a thing.
jayrdub
DI doesn't implement the Strategy pattern - you do. You must write your Strategy implementation as well as the code that consumes it. All a DI Container can and should do is to ensure that these two meet each other.
Mark Seemann
+2  A: 

One common way to implement this is as Mark described. You have a class that takes in an array of all concrete instances (it must be an array for StructureMap to behave as expected), and then uses some logic to figure out which instance to use.

Some sample code you can paste into a console program or unit test:

var container = new Container(x => x.Scan(scan =>
{
    scan.TheCallingAssembly();
    scan.WithDefaultConventions();
    scan.AddAllTypesOf<IDiscountCalculator>();
}));
var strategy = container.GetInstance<IDiscountStrategy>();
Console.WriteLine(strategy.GetDiscount("Regular", 10)); // 0
Console.WriteLine(strategy.GetDiscount("Normal", 10)); // 1
Console.WriteLine(strategy.GetDiscount("Special", 10)); // 5

which depends on the following types:

public interface IDiscountStrategy 
{
    decimal GetDiscount(string userType, decimal orderTotal);
}

public class DiscountStrategy : IDiscountStrategy
{
    private readonly IDiscountCalculator[] _discountCalculators;

    public DiscountStrategy(IDiscountCalculator[] discountCalculators)
    {
        _discountCalculators = discountCalculators;
    }

    public decimal GetDiscount(string userType, decimal orderTotal)
    {
        var calculator = _discountCalculators.FirstOrDefault(x => x.AppliesTo(userType));
        if (calculator == null) return 0;
        return calculator.CalculateDiscount(orderTotal);
    }
}

public interface IDiscountCalculator
{
    bool AppliesTo(string userType);
    decimal CalculateDiscount(decimal orderTotal);
}

public class NormalUserDiscountCalculator : IDiscountCalculator
{
    public bool AppliesTo(string userType)
    {
        return userType == "Normal";
    }

    public decimal CalculateDiscount(decimal orderTotal)
    {
        return orderTotal * 0.1m;
    }
}

public class SpecialUserDiscountCalculator : IDiscountCalculator
{
    public bool AppliesTo(string userType)
    {
        return userType == "Special";
    }

    public decimal CalculateDiscount(decimal orderTotal)
    {
        return orderTotal * 0.5m;
    }
}
Joshua Flanagan
By putting the AppliesTo() method in there, doesn't that embed knowledge of its external usage inside each DiscountCalculator? For some reason I figured that there might be something like the Profiles feature that could be leveraged in order to eliminate that Strategy class. Where you could do something like ObjectFactory.GetInstance<IDiscountCalculator>("NameOfProfile")
jayrdub
Also, what if the DiscountCalculators had some dependencies of their own, but varied also depending on the userType? How would you get a whole graph to load up with any concrete potentially varying on the userType?
jayrdub
Yes, you can create named instances, which is the other common way to solve this problem. You provide the name of the instance (not profile) when retrieving the instance from the container. I would recommend injecting an IContainer in your class and call GetInstance<> on that, instead of making direct calls to ObjectFactory.With named instances, you could configure the entire graph for that instance.
Joshua Flanagan
Thanks Joshua, did I implement what you are describing in my EDIT correctly?
jayrdub
That's not what I had in mind, but I suppose if that is what you need (2 entirely different configured containers), it should work.I just meant having named services that you would service locate within your classes:continer.GetInstance<IService>("VersionB")where container is an IContainer inject in your constructor (as opposed to calling ObjectFactory directly within your classes).
Joshua Flanagan