One interface, multiple implementations

One interface, multiple implementations

Contents
I. origins: one interface, multiple implementations
Second, to filter the target service based on the current context
Third, this program has done more generic Yidian
Fourth, if we have the wrong direction?

A, Origin: an interface, multiple implementations

Last week, the company had a training on .NET Core dependency injection, someone mentioned a problem: If the same service implementation type interface, you need to register multiple services, consumer services that will achieve the corresponding dynamic selection based on the current context . The question I will be frequently asked, we might as well use a simple example to describe the problem. Suppose we need to use ASP.NET Core MVC to develop a front-end consumer applications for micro-services, a special function, it needs for consumer applications using different types of processing logic. We'll call this function abstracted interfaces IFoobar, specific functions implemented in InvokeAsync method.

public interface IFoobar
{
    Task InvokeAsync(HttpContext httpContext);
}

Suppose a request from App, and applets, this function have different processing logic, for which they are implemented in a corresponding implementation of the types Foo and Bar.

public class Foo : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
}

public class Bar : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
}

Second, based on the current context filtering target service

Request a service call will carry the application type (App or MiniApp) information, now we need to address is: How to choose a corresponding service (Foo or Bar) according to the type of applications. In order to implement the mapping between the service type and the type of application, we choose Foo and Bar on the type of application follows this InvocationSourceAttribute, its Source property indicates the type of application calls the source.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class InvocationSourceAttribute : Attribute
{
    public string Source { get; }
    public InvocationSourceAttribute(string source) => Source = source;
}

[InvocationSource("App")]
public class Foo : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
}

[InvocationSource("MiniApp")]
public class Bar : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
}

So how context for the current request application type set and get it? This may represent an additional HttpContext object corresponding to the current request Feature is achieved. To this end we define as the IInvocationSourceFeature interfaces, InvocationSourceFeature the default implementation type. IInvocationSourceFeature property types represent members Source application calls the source. GetInvocationSource and SetInvocationSource use this Feature to get and set the type of application for the extension method of HttpContext.

public interface IInvocationSourceFeature
{
    string Source { get; }
}

public class InvocationSourceFeature : IInvocationSourceFeature
{
    public string Source { get; }
    public InvocationSourceFeature(string source) => Source = source;
        
}

public static class HttpContextExtensions
{
    public static string GetInvocationSource(this HttpContext httpContext) => httpContext.Features.Get<IInvocationSourceFeature>()?.Source;
    public static void SetInvocationSource(this HttpContext httpContext, string source) => httpContext.Features.Set<IInvocationSourceFeature>(new InvocationSourceFeature(source));
}

Now we will "select service" to achieve an equally realized on FoobarSelector type IFoobar interface in the following. The following code fragment, InvokeAsync implemented method FoobarSelector GetInvocationSource extension method will be called first acquisition application type defined above, and then utilized as IServiceProvider DI container can be realized in all service instances IFoobar interface. The next task is to select a target service by analyzing the characteristics InvocationSourceAttribute application on the service type.

public class FoobarSelector : IFoobar
{
    private static ConcurrentDictionary<Type, string> _sources = new ConcurrentDictionary<Type, string>();

    public Task InvokeAsync(HttpContext httpContext)
    {
        return httpContext.RequestServices.GetServices<IFoobar>()
            .FirstOrDefault(it => it != this && GetInvocationSource(it) == httpContext.GetInvocationSource())?.InvokeAsync(httpContext);
        string GetInvocationSource(object service)
        {
            var type = service.GetType();
            return _sources.GetOrAdd(type, _ => type.GetCustomAttribute<InvocationSourceAttribute>()?.Source);
        }
    }
}

We signed up for the three types of IFoobar achieved in accordance with the following manner. Since FoobarSelector last registered as a service, according to " catch up principle", if we use the DI container to obtain a service instance for IFoobar interface, returns will be a FoobarSelector object. We direct injection IFoobar objects in the constructor's HomeController. In the Action method Index, we will bind the source parameter for the application type, before calling InvokeAsync method IFoobar object, we call the extension method SetInvocationSource to apply it to the current HttpContext.

public class Program
{
    public static void Main(string[] args)
    {
        new WebHostBuilder()
            .UseKestrel()
            .ConfigureServices(svcs => svcs
                .AddHttpContextAccessor()
                .AddSingleton<IFoobar, Foo>()
                .AddSingleton<IFoobar, Bar>()
                .AddSingleton<IFoobar, FoobarSelector>()
                .AddMvc())
            .Configure(app => app.UseMvc())
            .Build()
            .Run();
    }
}

public class HomeController: Controller
{
    private readonly IFoobar _foobar;
    public HomeController(IFoobar foobar) => _foobar = foobar;

    [HttpGet("/")]
    public Task Index(string source)
    {
        HttpContext.SetInvocationSource(source);
        return _foobar.InvokeAsync(HttpContext)??Task.CompletedTask;
    }
}

We run this program, and use the query string (? Source = App) form to specify the type of application, we can get the desired results.

image

Third, this program will do a little more common

We can be above this program in a more general point. Since the purpose of "service filter" is to determine whether the target type of service with the current request context matches , so we can be defined as the ServiceFilterAttribute characteristics. Specific filter implemented in the Match method ServiceFilterAttribute. Derived from the abstract class InvocationSourceAttribute filter characteristics to help us complete the application for the type of service. If desired filtering logic for the other elements, to define the corresponding derived class.

public abstract class ServiceFilterAttribute: Attribute
{
    public abstract bool Match(HttpContext httpContext);
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class InvocationSourceAttribute : ServiceFilterAttribute
{
    public string Source { get; }
    public InvocationSourceAttribute(string source) => Source = source;
    public override bool Match(HttpContext httpContext)=> httpContext.GetInvocationSource() == Source;
}

We still use registered an additional " select service " approach to complete the call for a matching service instances, and defined as the base class ServiceSelector <T> for such services. GetService method of the base class will help us choose the current HttpContext instance of a matching service based.

public abstract class ServiceSelector<T> where T:class
{
    private static ConcurrentDictionary<Type, ServiceFilterAttribute> _filters = new ConcurrentDictionary<Type, ServiceFilterAttribute>();
    private readonly IHttpContextAccessor _httpContextAccessor;
    protected ServiceSelector(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;

    protected T GetService()
    {
        var httpContext = _httpContextAccessor.HttpContext;
        return httpContext.RequestServices.GetServices<T>()
            .FirstOrDefault(it => it != this && GetFilter(it)?.Match(httpContext) == true);
        ServiceFilterAttribute GetFilter(object service)
        {
            var type = service.GetType();
            return _filters.GetOrAdd(type, _ => type.GetCustomAttribute<ServiceFilterAttribute>());
        }
    }
}

For IFoobar "Service Selector" you need to be rewritten accordingly. The following code fragment, FoobarSelector InvokeAsync following method Chengzi Ji class ServiceSelector <IFoobar>, in InvokeAsync implemented method, the method calls the base class GetService service instance obtained screening out, it only needs to call the same name .

public class FoobarSelector : ServiceSelector<IFoobar>, IFoobar
{
    public FoobarSelector(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) { }
    public Task InvokeAsync(HttpContext httpContext) => GetService()?.InvokeAsync(httpContext);
}

Fourth, are we in the wrong direction?

We can even be the ultimate solution to the above: For example, we can use the following form to achieve on the type of application InvocationSourceAttribute plus information services registered (type of service and life cycle), then we can complete the bulk registration for these types of services . We can also be employed to generate IL Emit the dynamically select the type of service (such as the above FoobarSelector) corresponding to, and register it to the DI framework, so that the application does not need to write any code registered in the service.

[InvocationSource("App", ServiceLifetime.Singleton, typeof(IFoobar))]
public class Foo : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
}

[InvocationSource("MiniApp", ServiceLifetime.Singleton, typeof(IFoobar))]
public class Bar : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
}

So far, our solution looks pretty good (in addition to the need to create all service instances), expansion flexibility, programming elegance, but I think we have the wrong direction . Because the target service from beginning to end we only focus on the dimension represented IFoobar, so we always had in mind: how to use the DI container provides a target service instance. But the core problem we face is actually: how to provide the service instance matching the current context, this is a matter of "service instance" About dimension. After "dimension upgrade", the corresponding solution ideas is very clear: if it wants to solve the problem is to provide IFoobar for instance, we only need to define the following IFoobarProvider, and use it GetService method of providing service instance we want it. FoobarProvider represents the realization of the default interface.

public interface IFoobarProvider
{
    IFoobar GetService();
}

public sealed class FoobarProvider : IFoobarProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public FoobarProvider(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
    public IFoobar GetService()
    {
        switch (_httpContextAccessor.HttpContext.GetInvocationSource())
        {
            case "App": return new Foo();
            case "MiniApp": return new Bar();
            default: return null;
        }
    }
}

The use of the services required to provide IFoobarProvider instance, our program also will be very simple.

public class Program
{
    public static void Main(string[] args)
    {
        new WebHostBuilder()
            .UseKestrel()
            .ConfigureServices(svcs => svcs
                .AddHttpContextAccessor()
                 .AddSingleton<IFoobarProvider, FoobarProvider>()
                .AddMvc())
            .Configure(app => app.UseMvc())
            .Build()
            .Run();
    }
}

public class HomeController: Controller
{
    private readonly IFoobarProvider  _foobarProvider;
    public HomeController(IFoobarProvider foobarProvider)=> _foobarProvider = foobarProvider;

    [HttpGet("/")]
    public Task Index(string source)
    {
        HttpContext.SetInvocationSource(source);
        return _foobarProvider.GetService()?.InvokeAsync(HttpContext)??Task.CompletedTask;
    }
}

"Three-body" Let us understand what is the "dimensionality reduction attack", you need to go against the norm in software design. For an issue, if not effectively addressed, could consider the possibility of a rise dimension, can often find a shortcut from the perspective of high-dimensional look at the problem. Software design is abstract art, "l-dimensional attack" is actually a "dimension" nothing more abstract level.

image

Author: Jiang Jinnan 
micro-channel public account: A Ouchi old

Contents
I. origins: one interface, multiple implementations
Second, to filter the target service based on the current context
Third, this program has done more generic Yidian
Fourth, if we have the wrong direction?

A, Origin: an interface, multiple implementations

Last week, the company had a training on .NET Core dependency injection, someone mentioned a problem: If the same service implementation type interface, you need to register multiple services, consumer services that will achieve the corresponding dynamic selection based on the current context . The question I will be frequently asked, we might as well use a simple example to describe the problem. Suppose we need to use ASP.NET Core MVC to develop a front-end consumer applications for micro-services, a special function, it needs for consumer applications using different types of processing logic. We'll call this function abstracted interfaces IFoobar, specific functions implemented in InvokeAsync method.

public interface IFoobar
{
    Task InvokeAsync(HttpContext httpContext);
}

Suppose a request from App, and applets, this function have different processing logic, for which they are implemented in a corresponding implementation of the types Foo and Bar.

public class Foo : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
}

public class Bar : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
}

Second, based on the current context filtering target service

Request a service call will carry the application type (App or MiniApp) information, now we need to address is: How to choose a corresponding service (Foo or Bar) according to the type of applications. In order to implement the mapping between the service type and the type of application, we choose Foo and Bar on the type of application follows this InvocationSourceAttribute, its Source property indicates the type of application calls the source.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class InvocationSourceAttribute : Attribute
{
    public string Source { get; }
    public InvocationSourceAttribute(string source) => Source = source;
}

[InvocationSource("App")]
public class Foo : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
}

[InvocationSource("MiniApp")]
public class Bar : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
}

So how context for the current request application type set and get it? This may represent an additional HttpContext object corresponding to the current request Feature is achieved. To this end we define as the IInvocationSourceFeature interfaces, InvocationSourceFeature the default implementation type. IInvocationSourceFeature property types represent members Source application calls the source. GetInvocationSource and SetInvocationSource use this Feature to get and set the type of application for the extension method of HttpContext.

public interface IInvocationSourceFeature
{
    string Source { get; }
}

public class InvocationSourceFeature : IInvocationSourceFeature
{
    public string Source { get; }
    public InvocationSourceFeature(string source) => Source = source;
        
}

public static class HttpContextExtensions
{
    public static string GetInvocationSource(this HttpContext httpContext) => httpContext.Features.Get<IInvocationSourceFeature>()?.Source;
    public static void SetInvocationSource(this HttpContext httpContext, string source) => httpContext.Features.Set<IInvocationSourceFeature>(new InvocationSourceFeature(source));
}

Now we will "select service" to achieve an equally realized on FoobarSelector type IFoobar interface in the following. The following code fragment, InvokeAsync implemented method FoobarSelector GetInvocationSource extension method will be called first acquisition application type defined above, and then utilized as IServiceProvider DI container can be realized in all service instances IFoobar interface. The next task is to select a target service by analyzing the characteristics InvocationSourceAttribute application on the service type.

public class FoobarSelector : IFoobar
{
    private static ConcurrentDictionary<Type, string> _sources = new ConcurrentDictionary<Type, string>();

    public Task InvokeAsync(HttpContext httpContext)
    {
        return httpContext.RequestServices.GetServices<IFoobar>()
            .FirstOrDefault(it => it != this && GetInvocationSource(it) == httpContext.GetInvocationSource())?.InvokeAsync(httpContext);
        string GetInvocationSource(object service)
        {
            var type = service.GetType();
            return _sources.GetOrAdd(type, _ => type.GetCustomAttribute<InvocationSourceAttribute>()?.Source);
        }
    }
}

We signed up for the three types of IFoobar achieved in accordance with the following manner. Since FoobarSelector last registered as a service, according to " catch up principle", if we use the DI container to obtain a service instance for IFoobar interface, returns will be a FoobarSelector object. We direct injection IFoobar objects in the constructor's HomeController. In the Action method Index, we will bind the source parameter for the application type, before calling InvokeAsync method IFoobar object, we call the extension method SetInvocationSource to apply it to the current HttpContext.

public class Program
{
    public static void Main(string[] args)
    {
        new WebHostBuilder()
            .UseKestrel()
            .ConfigureServices(svcs => svcs
                .AddHttpContextAccessor()
                .AddSingleton<IFoobar, Foo>()
                .AddSingleton<IFoobar, Bar>()
                .AddSingleton<IFoobar, FoobarSelector>()
                .AddMvc())
            .Configure(app => app.UseMvc())
            .Build()
            .Run();
    }
}

public class HomeController: Controller
{
    private readonly IFoobar _foobar;
    public HomeController(IFoobar foobar) => _foobar = foobar;

    [HttpGet("/")]
    public Task Index(string source)
    {
        HttpContext.SetInvocationSource(source);
        return _foobar.InvokeAsync(HttpContext)??Task.CompletedTask;
    }
}

We run this program, and use the query string (? Source = App) form to specify the type of application, we can get the desired results.

image

Third, this program will do a little more common

We can be above this program in a more general point. Since the purpose of "service filter" is to determine whether the target type of service with the current request context matches , so we can be defined as the ServiceFilterAttribute characteristics. Specific filter implemented in the Match method ServiceFilterAttribute. Derived from the abstract class InvocationSourceAttribute filter characteristics to help us complete the application for the type of service. If desired filtering logic for the other elements, to define the corresponding derived class.

public abstract class ServiceFilterAttribute: Attribute
{
    public abstract bool Match(HttpContext httpContext);
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class InvocationSourceAttribute : ServiceFilterAttribute
{
    public string Source { get; }
    public InvocationSourceAttribute(string source) => Source = source;
    public override bool Match(HttpContext httpContext)=> httpContext.GetInvocationSource() == Source;
}

我们依然采用注册一个额外的“选择服务”的方式来完成针对匹配服务实例的调用,并为这样的服务定义了如下这个基类ServiceSelector<T>。这个基类提供的GetService方法会帮助我们根据当前HttpContext选择出匹配的服务实例。

public abstract class ServiceSelector<T> where T:class
{
    private static ConcurrentDictionary<Type, ServiceFilterAttribute> _filters = new ConcurrentDictionary<Type, ServiceFilterAttribute>();
    private readonly IHttpContextAccessor _httpContextAccessor;
    protected ServiceSelector(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;

    protected T GetService()
    {
        var httpContext = _httpContextAccessor.HttpContext;
        return httpContext.RequestServices.GetServices<T>()
            .FirstOrDefault(it => it != this && GetFilter(it)?.Match(httpContext) == true);
        ServiceFilterAttribute GetFilter(object service)
        {
            var type = service.GetType();
            return _filters.GetOrAdd(type, _ => type.GetCustomAttribute<ServiceFilterAttribute>());
        }
    }
}

针对IFoobar的“服务选择器”则需要作相应的改写。如下面的代码片段所示,FoobarSelector 继承自基类ServiceSelector<IFoobar>,在实现的InvokeAsync方法中,在调用基类的GetService方法得到筛选出来的服务实例后,它只需要调用同名的InvokeAsync方法即可。

public class FoobarSelector : ServiceSelector<IFoobar>, IFoobar
{
    public FoobarSelector(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) { }
    public Task InvokeAsync(HttpContext httpContext) => GetService()?.InvokeAsync(httpContext);
}

四、我们是否走错了方向?

我们甚至可以将上面解决方案做到极致:比如我们可以采用如下的形式在实现类型上应用的InvocationSourceAttribute加上服务注册的信息(服务类型和生命周期),那么就可以批量完成针对这些类型的服务注册。我们还可以采用IL Emit的方式动态生成对应的服务选择器类型(比如上面的FoobarSelector),并将它注册到依赖注入框架,这样应用程序就不需要编写任何服务注册的代码了。

[InvocationSource("App", ServiceLifetime.Singleton, typeof(IFoobar))]
public class Foo : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
}

[InvocationSource("MiniApp", ServiceLifetime.Singleton, typeof(IFoobar))]
public class Bar : IFoobar
{
    public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
}

到目前为止,我们的解决方案貌似还不错(除了需要创建所有服务实例之外),扩展灵活,编程优雅,但是我觉得我们走错了方向。由于我们自始自终关注的维度只有IFoobar代表的目标服务,所以我们脑子里想的始终是:如何利用DI容器提供目标服务实例。但是我们面临的核心问题其实是:如何根据当前上下文提供与之匹配的服务实例,这是一个关于“服务实例的提供”维度的问题。“维度提升”之后,对应的解决思路就很清晰了:既然要解决的是针对IFoobar实例的提供问题,我们只需要定义如下IFoobarProvider,并利用它的GetService方法提供我们希望的服务实例就可以了。FoobarProvider表示对该接口的默认实现。

public interface IFoobarProvider
{
    IFoobar GetService();
}

public sealed class FoobarProvider : IFoobarProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public FoobarProvider(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
    public IFoobar GetService()
    {
        switch (_httpContextAccessor.HttpContext.GetInvocationSource())
        {
            case "App": return new Foo();
            case "MiniApp": return new Bar();
            default: return null;
        }
    }
}

采用用来提供所需服务实例的IFoobarProvider,我们的程序同样会很简单。

public class Program
{
    public static void Main(string[] args)
    {
        new WebHostBuilder()
            .UseKestrel()
            .ConfigureServices(svcs => svcs
                .AddHttpContextAccessor()
                 .AddSingleton<IFoobarProvider, FoobarProvider>()
                .AddMvc())
            .Configure(app => app.UseMvc())
            .Build()
            .Run();
    }
}

public class HomeController: Controller
{
    private readonly IFoobarProvider  _foobarProvider;
    public HomeController(IFoobarProvider foobarProvider)=> _foobarProvider = foobarProvider;

    [HttpGet("/")]
    public Task Index(string source)
    {
        HttpContext.SetInvocationSource(source);
        return _foobarProvider.GetService()?.InvokeAsync(HttpContext)??Task.CompletedTask;
    }
}

"Three-body" Let us understand what is the "dimensionality reduction attack", you need to go against the norm in software design. For an issue, if not effectively addressed, could consider the possibility of a rise dimension, can often find a shortcut from the perspective of high-dimensional look at the problem. Software design is abstract art, "l-dimensional attack" is actually a "dimension" nothing more abstract level.

image

Guess you like

Origin www.cnblogs.com/Leo_wl/p/11070543.html