Стратегия определения правильной реализации интерфейса в мультитенантной среде

Учитывая этот интерфейс:

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();
}

и 2 внедрения этого:

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();
        }
    }
}

Учитывая приведенный выше простой код, допустим, что реализация метода Calculate в каждой компании будет разной. Как правильно загрузить сборки во время инициализации и вызвать правильный метод правильной сборки? Я выяснил, с какой легкостью можно определить, к какой компании относится запрос, теперь мне просто нужно вызвать правильный метод, соответствующий текущему бизнесу.

Спасибо Стивен

Обновленный пример кода

Большое спасибо @Scott, вот изменения, которые я должен был сделать, чтобы принятый ответ работал правильно.

В этом случае мне нужно было использовать Сборочный ассемблер, чтобы найти мой тип. Обратите внимание, что я использовал атрибут, чтобы отметить свою сборку, чтобы фильтрация на его основе была проще и менее подвержена ошибкам.

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;
}

Атрибут Маркер

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

Использование в сборке плагина

[assembly: TypeMarker]

И, наконец, небольшое изменение статических MyTypes для поддержки квалифицированного имени.

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";
}

1 ответ

Решение

Я не думаю, что есть простое или особенно элегантное решение, потому что ServiceStack разрешает свои сервисы на основе конкретных классов, а не интерфейсов, и это нечто за пределами возможностей Funq. Однако это не невозможно.

Вам понадобится реализация по умолчанию для каждого интерфейса, который вы хотите использовать в качестве DTO, потому что ServiceStack разрешается с использованием конкретного класса.

Так что по сути здесь у нас есть DefaultCalculator который предоставит нам путь к нашему методу действий.

[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();
    }
}

Тогда наш метод действия используется почти как обычно, за исключением того, что мы вызываем метод GetInstance<T> который мы реализуем в нашем MyServiceBase от которого эта услуга распространяется, а не Serviceпотому что это облегчает совместное использование этого метода между службами.

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;
    }
}

В MyServiceBase мы реализуем метод GetInstance<T> который отвечает за разрешение правильного экземпляра, основанного на имени клиента, Tв данном случае это ILoanCalculator,

Метод работает по:

  1. Определите имя клиента по Request.GetItem("customer"), Ваш текущий метод должен будет установить идентификатор клиента на RequestItems коллекции с использованием Request.SetItem метод в точке, где вы идентифицируете своего клиента. Или, возможно, переместить механизм идентификации в этот метод.

  2. Когда имя клиента известно, можно создать полное имя типа на основе переданного шаблона имени типа. т.е. MyCompany.Services.Business.Foo.LoanCalculator где Foo это клиент. Это должно разрешить тип, если содержащая сборка была загружена при запуске.

  3. Затем экземпляр типа создается как T т.е. интерфейс, ILoanCalculator

  4. Затем проверка безопасности, чтобы убедиться, что все работает хорошо.

  5. Затем заполните значения из запроса, которые находятся в DefaultCalculator который также имеет тип ILoanCalculator,

  6. Верните экземпляр.

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;
    }
}

Вы можете по желанию добавить кэш экземпляров, чтобы избежать необходимости использовать Activator.CreateInstance за каждый запрос.

Если у вас будет много разных типов, создаваемых динамически, вы можете организовать их строки типов в статический класс:

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

Тогда все, что осталось сделать, это убедиться, что вы добавляете сборки с различными реализациями клиентов, загруженные в ваше приложение, что вы можете сделать из своего AppHostConfigure метод.

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

Очевидно, что этот метод основывается на полном имени типа определенного формата, чтобы сопоставить его с клиентами. Есть и другие подходы, но я считаю, что это просто.

Надеюсь, это поможет.

Другие вопросы по тегам