1

Given this interface:

public interface ILoanCalculator
{
    decimal Amount { get; set; }
    decimal TermYears { get; set; }
    int TermMonths { get; set; }
    decimal IntrestRatePerYear { get; set; }
    DateTime StartDate { get; set; }
    decimal MonthlyPayments { get; set; }
    void Calculate();
}

and 2 implentations of it:

namespace MyCompany.Services.Business.Foo
{
    public interface ILoanCalculator : Common.ILoanCalculator
    {

    }

    public class LoanCalculator : ILoanCalculator
    {
        public decimal Amount { get; set; }
        public decimal TermYears { get; set; }
        public int TermMonths { get; set; }
        public decimal IntrestRatePerYear { get; set; }
        public DateTime StartDate { get; set; }
        public decimal MonthlyPayments { get; set; }
        public void Calculate()
        {
            throw new NotImplementedException();
        }
    }
}

namespace MyCompany.Services.Business.Bar
{
    public interface ILoanCalculator : Common.ILoanCalculator
    {

    }

    public class LoanCalculator : ILoanCalculator
    {
        public decimal Amount { get; set; }
        public decimal TermYears { get; set; }
        public int TermMonths { get; set; }
        public decimal IntrestRatePerYear { get; set; }
        public DateTime StartDate { get; set; }
        public decimal MonthlyPayments { get; set; }
        public void Calculate()
        {
            throw new NotImplementedException();
        }
    }
}

Given the simple code from above, lets say that the implementation of Calculate method will be different per company. What is the proper way to load the assemblies during initialization and call the correct method of the correct assembly? I have figured out the easy part with is determining which company the request is for, now I just need to call the correct method that corresponds to the current Business.

Thank you, Stephen

Updated Example Code

Big shout out to @Scott, here are the changes I had to make in order for the accepted answer to work correctly.

In this case I had to use the Assembly Resolver to find my type. Note that I used an attribute to mark my assembly so that filtering based on it was simpler and less error prone.

public T GetInstance<T>(string typeName, object value) where T : class
{
    // Get the customer name from the request items
    var customer = Request.GetItem("customer") as string;
    if (customer == null) throw new Exception("Customer has not been set");

    // Create the typeof the object from the customer name and the type format
    var assemblyQualifiedName = string.Format(typeName, customer);
    var type = Type.GetType(
        assemblyQualifiedName,
        (name) =>
        {
            return AppDomain.CurrentDomain.GetAssemblies()
                .Where(a => a.GetCustomAttributes(typeof(TypeMarkerAttribute), false).Any()).FirstOrDefault();
        },
        null,
        true);

    if (type == null) throw new Exception("Customer type not loaded");

    // Create an instance of the type
    var instance = Activator.CreateInstance(type) as T;

    // Check the instance is valid
    if (instance == default(T)) throw new Exception("Unable to create instance");

    // Populate it with the values from the request
    instance.PopulateWith(value);

    // Return the instance
    return instance;
}

Marker Attribute

[AttributeUsage(AttributeTargets.Assembly)]
public class TypeMarkerAttribute : Attribute { }

Usage in plugin assembly

[assembly: TypeMarker]

And finally, a slight change to the static MyTypes to support qualified name

public static class MyTypes
{
    // assemblyQualifiedName
    public static string LoanCalculator = "SomeName.BusinessLogic.{0}.LoanCalculator, SomeName.BusinessLogic.{0}, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
}
Stephen Patten
  • 6,333
  • 10
  • 50
  • 84

1 Answers1

3

I don't think there is an easy or particularly elegant solution, because ServiceStack resolves it's services based on concrete classes rather than by interfaces, and this is something beyond the capability of Funq. However it's not impossible.

You will need to have a default implementation of each interface that you want to use as a DTO, because ServiceStack resolves using the concrete class.

So essentially here we have a DefaultCalculator which will provide us the route into our action method.

[Route("/Calculate","GET")]
public class DefaultCalculator : ILoanCalculator
{
    public decimal Amount { get; set; }
    public decimal TermYears { get; set; }
    public int TermMonths { get; set; }
    public decimal IntrestRatePerYear { get; set; }
    public DateTime StartDate { get; set; }
    public decimal MonthlyPayments { get; set; }
    public void Calculate()
    {
        throw new NotImplementedException();
    }
}

Then our action method is used almost as normal, except we call a method GetInstance<T> which we implement in our MyServiceBase from which this service extends, rather than Service, because it makes it easier to share this method across services.

public class TestService : MyServiceBase
{
    public decimal Get(DefaultCalculator request)
    {
        // Get the instance of the calculator for the current customer
        var calculator = GetInstance<ILoanCalculator>(MyTypes.LoanCalculator, request);

        // Perform the action
        calculator.Calculate();

        // Return the result
        return calculator.MonthlyPayments;
    }
}

In MyServiceBase we implement the method GetInstance<T> which is responsible for resolving the correct instance, based on the customer name, of T, in this case is the ILoanCalculator.

The method works by:

  1. Determine the customer name from the Request.GetItem("customer"). Your current method will need to set the customer identifier on the Request Items collections using Request.SetItem method at the point where you identify your customer. Or perhaps move the identification mechanism into this method.

  2. With the customer name known the full type name can be built, based on the passed in type name template. i.e. MyCompany.Services.Business.Foo.LoanCalculator where Foo is the customer. This should resolve the type if the containing assembly has been loaded at startup.

  3. Then an instance of the type is created as T i.e. the interface, ILoanCalculator

  4. Then a safety check to make sure everything worked okay.

  5. Then populate the values from the request that are in DefaultCalculator which is also of type ILoanCalculator.

  6. Return the instance.

public class MyServiceBase : Service
{
    public T GetInstance<T>(string typeName, object value)
    {
        // Get the customer name from the request items
        var customer = Request.GetItem("customer") as string;
        if(customer == null) throw new Exception("Customer has not been set");

        // Create the typeof the object from the customer name and the type format
        var type = Type.GetType(string.Format(typeName, customer));

        // Create an instance of the type
        var instance = Activator.CreateInstance(type) as T;

        // Check the instance is valid
        if(instance == default(T)) throw new Exception("Unable to create instance");

        // Populate it with the values from the request
        instance.PopulateWith(value);

        // Return the instance
        return instance;
    }
}

You can optionally add a cache of instances to prevent having to use Activator.CreateInstance for each request.

If you are going to have many different types being created dynamically then you may wish to organise their type strings into a static class:

public static class MyTypes
{
    public static string LoanCalculator = "MyCompany.Services.Business.{0}.LoanCalculator";
    public static string AnotherType = "MyCompany.Services.Business.{0}.AnotherType";
    //...//
}

Then all that's left to do is ensure that you add the assemblies with your different customer implementations are loaded into your application, which you could do from your AppHost Configure method.

foreach(var pluginFileName in Directory.GetFiles("Plugins", "*.dll"))
    Assembly.Load(File.ReadAllBytes(pluginFileName));

Obviously this method relies on the full type name being of a specific format to match it to customers. There are other approaches but I believe this to be straightforward.

I hope that helps.

Stephen Patten
  • 6,333
  • 10
  • 50
  • 84
Scott
  • 21,211
  • 8
  • 65
  • 72
  • working through your very detailed answer, thank you. – Stephen Patten Aug 12 '14 at 17:55
  • @StephenPatten Awesome. You're welcome. Let me know if you have any issues. – Scott Aug 12 '14 at 18:36
  • the only 'issue' I can speak of is that Fusion is looking in the bin folder and not in memory for the type. – Stephen Patten Aug 12 '14 at 22:10
  • @StephenPatten You don't have to look in the bin folder for the type, if you have decided to build it along with your main assembly. That's entirely optional. You only have to ensure that the type exists. So if you want to include it along with your application thats fine, just ignore the plugin part. – Scott Aug 13 '14 at 07:37
  • I was never able to load and use a type from the plugin folder. The only time I could get the type to work was when everything was located in the bin folder – Stephen Patten Aug 16 '14 at 04:01