2016-08-26 17 views
62

tôi có các dịch vụ đó được bắt nguồn từ cùng một giao diệnLàm thế nào để đăng ký nhiều triển khai của cùng một giao diện trong Asp.Net Core?

public interface IService 
{ 
} 

public class ServiceA : IService 
{ 
} 

public class ServiceB : IService 
{  
} 

public class ServiceC : IService 
{  
} 

container IOC thường khác như Unity phép bạn đăng ký triển khai cụ thể của một số Key mà phân biệt chúng.

Trong Asp.Net Core, làm cách nào để đăng ký các dịch vụ này và giải quyết trong thời gian chạy dựa trên một số khóa?

Tôi không thấy bất kỳ phương pháp nào trong số Add Dịch vụ có tham số key hoặc name thường được sử dụng để phân biệt việc triển khai cụ thể.

public void ConfigureServices(IServiceCollection services) 
    {    
     // How do I register services here of the same interface    
    } 


    public MyController:Controller 
    { 
     public void DoSomeThing(string key) 
     { 
      // How do get service based on key 
     } 
    } 

Mẫu nhà máy là tùy chọn duy nhất ở đây?

Update1
Tôi đã đi mặc dù bài viết here cho thấy làm thế nào để sử dụng mô hình nhà máy để có được các trường hợp dịch vụ khi chúng tôi có nhiều thi concreate. Tuy nhiên nó vẫn không phải là giải pháp hoàn chỉnh. khi tôi gọi phương thức _serviceProvider.GetService() tôi không thể tiêm dữ liệu vào hàm tạo. Ví dụ: xem xét ví dụ này

public class ServiceA : IService 
{ 
    private string _efConnectionString; 
    ServiceA(string efconnectionString) 
    { 
     _efConnecttionString = efConnectionString; 
    } 
} 

public class ServiceB : IService 
{  
    private string _mongoConnectionString; 
    public ServiceB(string mongoConnectionString) 
    { 
     _mongoConnectionString = mongoConnectionString; 
    } 
} 

public class ServiceC : IService 
{  
    private string _someOtherConnectionString 
    public ServiceC(string someOtherConnectionString) 
    { 
     _someOtherConnectionString = someOtherConnectionString; 
    } 
} 

Làm cách nào để có thể kết nối chuỗi thích hợp? Trong Unity hoặc bất kỳ IOC nào khác, chúng tôi có thể làm điều đó tại thời điểm đăng ký loại. Tôi có thể sử dụng IOption tuy nhiên điều đó sẽ yêu cầu tôi phải tiêm tất cả các thiết lập, tôi không thể tiêm một chuỗi kết nối cụ thể vào dịch vụ. Cũng cần lưu ý rằng tôi đang cố gắng tránh sử dụng các vật chứa khác (bao gồm cả Unity) vì sau đó tôi phải đăng ký mọi thứ khác (ví dụ: Bộ điều khiển) với thùng chứa mới là tốt.

Cũng sử dụng mô hình nhà máy để tạo ra ví dụ dịch vụ là chống lại DIP như nhà máy tăng số lượng phụ thuộc một khách hàng buộc phải phụ thuộc vào details here

Vì vậy, tôi nghĩ mặc định DI trong ASP.NET lõi thiếu 2 điều
1> đăng ký các trường hợp sử dụng khóa
2> Tiêm dữ liệu tĩnh vào constructor trong đăng ký

+2

có thể trùng lặp của [Dependency Injection giải quyết theo tên] (http://stackoverflow.com/questions/39072001/dependency-injection -ký-by-name) –

+1

Cuối cùng cũng có [phần mở rộng trong nuget] (https://www.nuget.org/packages/Neleus.DependencyInjection.Extensions) để đăng ký dựa trên tên, hy vọng nó có thể giúp – neleus

+0

Xin chào, xin lỗi cho câu hỏi ngu ngốc của tôi, nhưng tôi mới về với Microso ft.Extensions.DependencyInjection ... bạn có nghĩ rằng tạo ra 3 giao diện trống để mở rộng Iservice như "giao diện công cộng IServiceA: IService" và hơn "public class ServiceA: IServiceA" ... có thể là một lựa chọn thực hành tốt không? –

Trả lời

35

Tôi đã làm một workaround đơn giản sử dụng Func khi tôi tìm thấy bản thân mình trong tình huống này.

services.AddTransient<ServiceA>(); 
services.AddTransient<ServiceB>(); 
services.AddTransient<ServiceC>(); 

services.AddTransient(factory => 
{ 
    Func<string, IService> accesor = key => 
    { 
     switch(key) 
     { 
      case "A": 
       return factory.GetService<ServiceA>(); 
      case "B": 
       return factory.GetService<ServiceB>(); 
      case "C": 
       return factory.GetService<ServiceC>(); 
      default: 
       throw new KeyNotFoundException(); // or maybe return null, up to you 
     } 
    }; 
    return accesor; 
}); 

Và sử dụng nó từ bất kỳ lớp đăng ký với DI như:

public class Consumer 
{ 
    private readonly Func<string, IService> _serviceAccessor; 

    public Consumer(Func<string, IService> serviceAccessor) 
    { 
     _serviceAccessor = serviceAccesor; 
    } 

    public void UseServiceA() 
    { 
     //use _serviceAccessor field to resolve desired type 
     _serviceAccessor("A").DoIServiceOperation(); 
    } 
} 

CẬP NHẬT

Hãy ghi nhớ rằng trong ví dụ này là chìa khóa để giải quyết là một chuỗi, vì lợi ích của sự đơn giản và bởi vì OP đã yêu cầu cho trường hợp này nói riêng.

Nhưng bạn có thể sử dụng bất kỳ loại độ phân giải tùy chỉnh nào làm khóa, vì bạn thường không muốn có một chuyển đổi n-case cực lớn mục nát mã của bạn. Phụ thuộc vào cách quy mô ứng dụng của bạn.

+0

Làm cách nào để bạn truy xuất một trong các dịch vụ theo chuỗi từ một lớp khác? –

+1

@MatthewStevenMonkan đã cập nhật câu trả lời của tôi bằng ví dụ –

+1

Sử dụng mẫu nhà máy như thế này là cách tốt nhất để đi. Cám ơn vì đã chia sẻ! –

5

Bạn đúng, việc xây dựng trong ASP.NET lõi chứa không có khái niệm về đăng ký nhiều dịch vụ và sau đó lấy một cụ thể, như bạn đề xuất, một nhà máy là sự trở lại duy nhất al giải pháp trong trường hợp đó.

Hoặc, bạn có thể chuyển sang vùng chứa của bên thứ ba như Unity hoặc StructureMap cung cấp giải pháp bạn cần (được ghi ở đây: https://docs.asp.net/en/latest/fundamentals/dependency-injection.html?#replacing-the-default-services-container).

11

Nó không được hỗ trợ bởi Microsoft.Extensions.DependencyInjection.

Nhưng bạn có thể cắm một cơ chế tiêm phụ thuộc khác, như StructureMapSee it's Home page và đó là GitHub Project.

Nó không khó chút nào:

  1. Thêm một phụ thuộc vào StructureMap trong bạn project.json:

    "Structuremap.Microsoft.DependencyInjection" : "1.0.1", 
    
  2. Tiêm nó vào trong đường ống ASP.NET bên ConfigureServices và đăng ký lớp học của bạn (see docs)

    public IServiceProvider ConfigureServices(IServiceCollection services) // returns IServiceProvider ! 
    { 
        // Add framework services. 
        services.AddMvc(); 
        services.AddWhatever(); 
    
        //using StructureMap; 
        var container = new Container(); 
        container.Configure(config => 
        { 
         // Register stuff in container, using the StructureMap APIs... 
         config.For<IPet>().Add(new Cat("CatA")).Named("A"); 
         config.For<IPet>().Add(new Cat("CatB")).Named("B"); 
         config.For<IPet>().Use("A"); // Optionally set a default 
         config.Populate(services); 
        }); 
    
        return container.GetInstance<IServiceProvider>(); 
    } 
    
  3. Sau đó, để có được một trường hợp được đặt tên, bạn sẽ cần phải yêu cầu IContainer

    public class HomeController : Controller 
    { 
        public HomeController(IContainer injectedContainer) 
        { 
         var myPet = injectedContainer.GetInstance<IPet>("B"); 
         string name = myPet.Name; // Returns "CatB" 
    

Vậy là xong.

Ví dụ để xây dựng, bạn cần

public interface IPet 
    { 
     string Name { get; set; } 
    } 

    public class Cat : IPet 
    { 
     public Cat(string name) 
     { 
      Name = name; 
     } 

     public string Name {get; set; } 
    } 
+0

Tôi đã thử phương pháp này, nhưng tôi nhận được lỗi thời gian chạy trên bộ điều khiển của tôi bởi vì IContainer không được tìm thấy trong các kế hoạch xây dựng. Có điều gì tôi phải làm để yêu cầu IContainer được bơm tự động không? – mohrtan

+0

BTW, tôi đang sử dụng StructureMap.Micorosoft.DependencyInjection 1.3.0. – mohrtan

+0

Bạn có trả lại vùng chứa mới trong ConfigureServices không? –

8

tôi đã phải đối mặt với cùng một vấn đề và muốn chia sẻ cách tôi giải quyết nó và tại sao.

Như bạn đã đề cập, có hai vấn đề. Đầu tiên:

Trong Asp.Net Lõi làm thế nào để đăng ký các dịch vụ và giải quyết nó ở runtime dựa trên một số quan trọng?

Vì vậy, chúng tôi có những tùy chọn nào? Folks đề nghị hai:

  • Sử dụng một nhà máy tùy chỉnh (như _myFactory.GetServiceByKey(key))

  • Sử dụng một công cụ DI (như _unityContainer.Resolve<IService>(key))

là mô hình Nhà máy lựa chọn duy nhất ở đây?

Thực tế cả hai tùy chọn đều là các nhà máy vì mỗi IoC Container cũng là một nhà máy (có thể cấu hình cao và phức tạp). Và có vẻ như với tôi rằng các lựa chọn khác cũng là các biến thể của mẫu Nhà máy.

Vì vậy, tùy chọn nào tốt hơn? Ở đây tôi đồng ý với @Sock người đã đề xuất sử dụng nhà máy tùy chỉnh và đó là lý do tại sao.

Trước tiên, tôi luôn cố gắng tránh thêm các phụ thuộc mới khi chúng không thực sự cần thiết. Vì vậy, tôi đồng ý với bạn trong thời điểm này. Hơn nữa, việc sử dụng hai khung công tác DI còn tồi tệ hơn việc tạo ra sự trừu tượng hóa nhà máy tùy chỉnh. Trong trường hợp thứ hai, bạn phải thêm phụ thuộc gói mới (như Unity) nhưng tùy thuộc vào giao diện nhà máy mới thì ít ác hơn ở đây. Ý tưởng chính của ASP.NET Core DI, tôi tin rằng, là sự đơn giản. Nó duy trì một tập hợp các tính năng tối thiểu sau KISS principle. Nếu bạn cần một số tính năng bổ sung thì hãy sử dụng hoặc sử dụng Plungin tương ứng để thực hiện tính năng mong muốn (Nguyên tắc đóng đã đóng).

Thứ hai, thường chúng ta cần phải tiêm nhiều phụ thuộc có tên cho dịch vụ đơn lẻ.Trong trường hợp Unity bạn có thể phải chỉ định tên cho các tham số hàm tạo (sử dụng InjectionConstructor). Đăng ký này sử dụng phản ánh và một số logic thông minh để đoán đối số cho hàm tạo. Điều này cũng có thể dẫn đến lỗi thời gian chạy nếu đăng ký không khớp với các đối số hàm tạo. Mặt khác, khi sử dụng nhà máy của riêng bạn, bạn có toàn quyền kiểm soát cách cung cấp các tham số của hàm tạo. Nó dễ đọc hơn và được giải quyết tại thời gian biên dịch. KISS principle một lần nữa.

Vấn đề thứ hai:

Làm thế nào có thể _serviceProvider.GetService() tiêm thích hợp chuỗi kết nối ?

Trước tiên, tôi đồng ý với bạn rằng tùy thuộc vào những điều mới như IOptions (và do đó trên gói Microsoft.Extensions.Options.ConfigurationExtensions) không phải là một ý tưởng hay. Tôi đã nhìn thấy một số thảo luận về IOptions, nơi có những ý kiến ​​khác nhau về lợi ích của nó. Một lần nữa, tôi cố gắng tránh thêm các phụ thuộc mới khi chúng không thực sự cần thiết. Có thực sự cần thiết? Tôi nghĩ không có. Nếu không, mỗi lần thực hiện sẽ phải phụ thuộc vào nó mà không cần bất kỳ nhu cầu rõ ràng nào từ việc thực hiện đó (đối với tôi có vẻ như vi phạm ISP, nơi tôi đồng ý với bạn). Điều này cũng đúng về tùy thuộc vào nhà máy nhưng trong trường hợp này, có thể tránh được.

Các ASP.NET lõi DI cung cấp một quá tải rất tốt đẹp cho mục đích đó:

var mongoConnection = //... 
var efConnection = //... 
var otherConnection = //... 
services.AddTransient<IMyFactory>(
      s => new MyFactoryImpl(
       mongoConnection, efConnection, otherConnection, 
       s.GetService<ISomeDependency1>(), s.GetService<ISomeDependency2>()))); 
+0

Xin chào, xin lỗi vì câu hỏi ngu ngốc của tôi, nhưng tôi mới về Microsoft.Extensions.DependencyInjection ... bạn có nghĩ rằng tạo ra 3 giao diện mở rộng Iservice như "public interface IServiceA: IService" và hơn "public class ServiceA: IServiceA "... có thể là một lựa chọn thực hành tốt? –

+0

Sử dụng động cơ DI khác không phải là điều ác ngay cả trong ý kiến ​​của Microsoft. Đó là lý do tại sao họ đã tạo ra khung DI mở rộng. Cố gắng đạt được những gì họ đã làm là phát minh lại bánh xe. Autofac, Unity, StructureMap, tất cả đều là các framework DI trưởng thành và tôi chắc chắn sẽ sử dụng một trong số chúng. Nhưng tất nhiên, tôi sẽ sử dụng một lớp trừu tượng nhỏ để giảm thiểu sự phụ thuộc trực tiếp của tôi vào bất kỳ thứ gì trong số đó. –

+1

@ emiliano-magliocca Nói chung, bạn không nên phụ thuộc vào các giao diện mà bạn không sử dụng (ISP), 'IServiceA' trong trường hợp của bạn. Vì bạn đang sử dụng các phương thức từ 'IService', bạn chỉ nên phụ thuộc vào' IService'. – neleus

17

Tùy chọn khác là sử dụng phương pháp tiện ích GetServices từ Microsoft.Extensions.DependencyInjection.

đăng ký dịch vụ của bạn như:

services.AddSingleton<IService, ServiceA>(); 
services.AddSingleton<IService, ServiceB>(); 
services.AddSingleton<IService, ServiceC>(); 

Sau đó giải quyết với một chút của LINQ:

var services = serviceProvider.GetServices<IService>(); 
var serviceB = services.First(o => o.GetType() == typeof(ServiceB)); 

hoặc

var serviceZ = services.First(o => o.Name.Equals("Z")); 

(giả định rằng IService có một tài sản chuỗi gọi là "Tên ")

Đảm bảo có using Microsoft.Extensions.DependencyInjection;

+0

Trong mã của bạn, nếu tôi chỉ giải quyết một dịch vụ, không phải dịch vụ, dịch vụ nào sẽ được trả lại? Dịch vụ tôi đăng ký trước hay muộn? –

+2

Không chắc chắn, nhưng tôi nghĩ rằng đó không phải là xác định. Bất kỳ kết quả bạn nhận được ngày hôm nay có thể thay đổi vào ngày mai, nó có vẻ không phải là một thực hành tốt. –

+0

Giải pháp khá hay. Nên ở trên đầu. –

1

Rõ ràng, bạn chỉ có thể tiêm IEnumerable giao diện dịch vụ của mình! Và sau đó tìm trường hợp bạn muốn sử dụng LINQ.

Ví dụ của tôi là dành cho dịch vụ SNS AWS nhưng bạn có thể thực hiện tương tự cho mọi dịch vụ được tiêm thực sự.

Startup

foreach (string snsRegion in Configuration["SNSRegions"].Split(',', StringSplitOptions.RemoveEmptyEntries)) 
{ 
    services.AddAWSService<IAmazonSimpleNotificationService>(
     string.IsNullOrEmpty(snsRegion) ? null : 
     new AWSOptions() 
     { 
      Region = RegionEndpoint.GetBySystemName(snsRegion) 
     } 
    ); 
} 

services.AddSingleton<ISNSFactory, SNSFactory>(); 

services.Configure<SNSConfig>(Configuration); 

SNSConfig

public class SNSConfig 
{ 
    public string SNSDefaultRegion { get; set; } 
    public string SNSSMSRegion { get; set; } 
} 

appsettings.json

"SNSRegions": "ap-south-1,us-west-2", 
    "SNSDefaultRegion": "ap-south-1", 
    "SNSSMSRegion": "us-west-2", 

SNS Factory

public class SNSFactory : ISNSFactory 
{ 
    private readonly SNSConfig _snsConfig; 
    private readonly IEnumerable<IAmazonSimpleNotificationService> _snsServices; 

    public SNSFactory(
     IOptions<SNSConfig> snsConfig, 
     IEnumerable<IAmazonSimpleNotificationService> snsServices 
     ) 
    { 
     _snsConfig = snsConfig.Value; 
     _snsServices = snsServices; 
    } 

    public IAmazonSimpleNotificationService ForDefault() 
    { 
     return GetSNS(_snsConfig.SNSDefaultRegion); 
    } 

    public IAmazonSimpleNotificationService ForSMS() 
    { 
     return GetSNS(_snsConfig.SNSSMSRegion); 
    } 

    private IAmazonSimpleNotificationService GetSNS(string region) 
    { 
     return GetSNS(RegionEndpoint.GetBySystemName(region)); 
    } 

    private IAmazonSimpleNotificationService GetSNS(RegionEndpoint region) 
    { 
     IAmazonSimpleNotificationService service = _snsServices.FirstOrDefault(sns => sns.Config.RegionEndpoint == region); 

     if (service == null) 
     { 
      throw new Exception($"No SNS service registered for region: {region}"); 
     } 

     return service; 
    } 
} 

public interface ISNSFactory 
{ 
    IAmazonSimpleNotificationService ForDefault(); 

    IAmazonSimpleNotificationService ForSMS(); 
} 

Bây giờ bạn có thể nhận được dịch vụ SNS cho khu vực mà bạn muốn phục vụ khách hàng của bạn hoặc bộ điều khiển

public class SmsSender : ISmsSender 
{ 
    private readonly IAmazonSimpleNotificationService _sns; 

    public SmsSender(ISNSFactory snsFactory) 
    { 
     _sns = snsFactory.ForSMS(); 
    } 

    ....... 
} 

public class DeviceController : Controller 
{ 
    private readonly IAmazonSimpleNotificationService _sns; 

    public DeviceController(ISNSFactory snsFactory) 
    { 
     _sns = snsFactory.ForDefault(); 
    } 

    ......... 
} 
0

tôi đã làm một cái gì đó tương tự như đôi khi trước, và tôi nghĩ rằng việc thực hiện là tốt đẹp.

Hãy nói rằng bạn có thể xử lý các tin nhắn đến, mỗi tin nhắn có một loại, vì vậy cách tiếp cận phổ biến ở đây đó là sử dụng một bộ chuyển mạch và làm những việc như thế này:

@Override 
public void messageArrived(String type, Message message) throws Exception { 
    switch(type) { 
     case "A": do Something with message; break; 
     case "B": do Something else with message; break; 
     ... 
    } 
} 

Chúng tôi có thể tránh được rằng mã chuyển đổi, và cũng có thể chúng ta có thể tránh được sự cần thiết phải sửa đổi mã rằng mỗi khi một loại mới được thêm vào, sau mô hình này:

@Override 
public void messageArrived(String type, Message message) throws Exception { 
    messageHandler.getMessageHandler(type).handle(message); 
} 

Đây là đẹp phải không? Vậy làm thế nào chúng ta có thể đạt được một cái gì đó như thế?

Đầu tiên chúng ta hãy xác định một chú thích và cũng là một giao diện

@Retention(RUNTIME) 
@Target({TYPE}) 
public @interface MessageHandler { 
    String value() 
} 

public interface Handler { 
    void handle(MqttMessage message); 
} 

và bây giờ chúng ta hãy sử dụng chú thích này trong một lớp học:

@MessageHandler("myTopic") 
public class MyTopicHandler implements Handler { 

    @override 
    public void handle(MqttMessage message) { 
     // do something with the message 
    } 
} 

Phần cuối cùng đó là để viết các lớp MessageHandler, trong lớp này bạn chỉ cần lưu trữ trong một bản đồ tất cả các trường hợp trình xử lý của bạn, khóa của bản đồ sẽ là tên chủ đề và giá trị và thể hiện của lớp.Về cơ bản bạn phương pháp getMessageHandler sẽ giống như thế:

public Handler getMessageHandler(String topic) { 
    if (handlerMap == null) { 
     loadClassesFromClassPath(); // This method should load from classpath all the classes with the annotation MessageHandler and build the handlerMap 
    } 
    return handlerMap.get(topic); 
} 

Những điều tốt với giải pháp này đó là rằng nếu bạn cần phải thêm một handler mới cho một tin nhắn mới, bạn chỉ cần phải viết các lớp mới, thêm các chú thích để lớp học và đó là tất cả. Phần còn lại của mã vẫn giữ nguyên vì nó chung chung và nó sử dụng sự phản chiếu để tải và tạo các cá thể.

Hy vọng điều này sẽ giúp bạn một chút.

0

Phương pháp tiếp cận nhà máy chắc chắn là khả thi. Một cách tiếp cận khác là sử dụng thừa kế để tạo ra các giao diện riêng lẻ kế thừa từ IService, triển khai thực hiện các giao diện kế thừa trong việc triển khai IService của bạn và đăng ký các giao diện được kế thừa thay vì cơ sở. Cho dù thêm một hệ thống phân cấp thừa kế hoặc nhà máy là mô hình "đúng" tất cả phụ thuộc vào những người bạn nói chuyện với. Tôi thường phải sử dụng mẫu này khi giao dịch với nhiều nhà cung cấp cơ sở dữ liệu trong cùng một ứng dụng sử dụng chung chung, chẳng hạn như IRepository<T>, làm cơ sở để truy cập dữ liệu.

Ví dụ giao diện và triển khai:

public interface IService 
{ 
} 

public interface IServiceA: IService 
{} 

public interface IServiceB: IService 
{} 

public IServiceC: IService 
{} 

public class ServiceA: IServiceA 
{} 

public class ServiceB: IServiceB 
{} 

public class ServiceC: IServiceC 
{} 

container:

container.Register<IServiceA, ServiceA>(); 
container.Register<IServiceB, ServiceB>(); 
container.Register<IServiceC, ServiceC>(); 
Các vấn đề liên quan