1

... Or a different way of implementing this:

I have an interface called ICommunicationProvider which is then extended by ISMSCommunicationProvider, IEmailCommunicationProvider, IWhatsAppCommunicationProvider, etc.

Each of those interfaces is a blueprint for implementation of different messaging providers, for SMS I might use Twilio, for Email I might want to use MailChimp of SMTP, etc.

I created a class, implementation of ISMSCommunicationProvider called TwilioSMSCommunicationProvider which will be used to create instances of Twilio Communication Providers in the system. There could be multiple of Twilio Communication Providers, each having different API key, SMS number etc - but all of them will have the same CommunicationProviderCode that links them to TwilioSMSCommunicationProvider implementation and also SettingsKeys dictionary that defines the amount of fields for settings, lets say 4.

Now, I also want to create TextLocalSMSCommunicationProvider which will also be an implementation of ISMSCommunicationProvider, will also have CommunicationProviderCode field, different than Twilio implementation, and SettingsKeys dictionary but with different amount of fields for settings.

How can I make sure that systemCode and settings are always implemented in every class that extends ISMSCommunicationProvider if those fields MUST be static?

They must be static because I am using reflection to populate dropdowns with those implementations, when instances of Communication Providers are being added to the System/DB. I can't instantiate those implementations because all I need is the CommunicationProviderCode and settingsKeys dictionary to know what settings fields I need to create.

Example below shows the Implementation with some static fields at the top that I am currently using to do what I need to do, but it's ugly - because I will have to REMEMBER to add them to every implementation of ISMSCommunicationProvider rather than being able to use the Interface to force their presence. If they were properties - I wouldn't be able to fetch them via reflection, as I'd have to create instances...

Is there a cleaner way of achieving what I need to achieve?

Example:

ICommunicationProvider

public interface ICommunicationProvider { /// <summary> /// Communication Provider. /// </summary> CommunicationProvider CommunicationProvider { get; } } 

ISMSCommunicationProvider

public interface ISMSCommunicationProvider : ICommunicationProvider { void Send(ISMS sms); Task SendAsync(ISMS sms, bool isExceptionReport = false); Task<bool> ReportException(Exception ex, bool isReceive); } 

TwilioSMSCommunicationProvider

public class TwilioSMSCommunicationProvider : ISMSCommunicationProvider { public static readonly string[] SettingsKeys = { "AccountSID", "AuthToken", "SMSNumber"}; public static readonly string CommunicationProviderCode = "TwilioSMSProvider"; public static readonly string CommunicationProviderName = "Twilio SMS Provider"; public static readonly CommunicationType CommunicationProviderType = CommunicationType.SMS; private readonly Dictionary<string, string> communicationProviderSettings; private readonly ICommunicationProviderService communicationProviderService; /// <summary> /// Constructor /// </summary> /// <param name="communicationProvider"></param> /// <param name="communicationProviderService"></param> public TwilioSMSCommunicationProvider(CommunicationProvider communicationProvider, ICommunicationProviderService communicationProviderService) { this.CommunicationProvider = communicationProvider; this.communicationProviderService = communicationProviderService; this.communicationProviderSettings = communicationProviderService.GetSettingsForCommunicationProvider(SettingsKeys); TwilioClient.Init(this.communicationProviderSettings["AccountSID"], this.communicationProviderSettings["AuthToken"]); } public CommunicationProvider CommunicationProvider { get; } /// <summary> /// Send SMS /// </summary> /// <param name="sms"></param> public void Send(ISMS sms) { var message = MessageResource.Create( body: sms.BodyText, from: new Twilio.Types.PhoneNumber(this.communicationProviderSettings["SMSNumber"]), to: new Twilio.Types.PhoneNumber(sms.To) ); Console.WriteLine(message.Sid); } 

To add some extra reference as to what I need to do; I first choose the Provider Type:

enter image description here

enter image description here

Once a Provider Type box is populated, the second select box fetches all the implementations of ICommunicationProvider via reflection that have CommunicationType.SMS enum in its static field, uses another two static fields CommunicationProviderName and CommunicationProviderCode and populates the dropdown:

enter image description here

Now, After filling the other details and saving, a CommunicationProvider is created in the DB along with empty settings that have also been declared in a static field in TwilioSMSCommunicationProvider called SettingsKeys, creates those settings in the DB to be filled in with values by the user:

enter image description here

The general idea of the end result is - Creating an implementation of either SMS or Email or WhatsApp communication provider interface in code, will be the only thing I will need to do to create brand new CommunicationProviders that utilise this implementation. The implementation dictates what settings I need for it and has all the details I need for it to appear in the select boxes etc. All I need to do is create new class, fill in the details in that class, implement methods - and it just works.

As you can see, it needs a lot of static fields in the implementations, which essentially should be present in each implementation of ISMSCommunicationProvider, they will just have different values. If factory pattern can achieve this in a much cleaner way - please can you point me in the right direction?

9
  • 4
    "They must be static because I am using reflection" -- I don't think you've taken a step far enough back yet. Interfaces and reflection don't go together; the whole point of using an interface is to provide compile-time support for polymorphic behavior. The whole point of using reflection is to bypass whatever compile-time restrictions you might have and operate on the run-time features of an object. The real answer to your question is "no, those fields don't have to be static, because you don't need to use reflection". Instead, use the factory pattern, in which each interface you may ... Commented Jul 22, 2021 at 18:11
  • 3
    ... want to instantiate is created by a factory object registered with some kind of factory manager. You can declare a factory interface to require the properties you will need for populating your drop-down. See duplicate for extensive discussion of the factory pattern, and when and how to use it. Commented Jul 22, 2021 at 18:11
  • 1
    I don't think that the suggested duplicate quite answers the question (even though I would agree that it may give some useful ideas at how to implement what OP wants). Commented Jul 22, 2021 at 18:20
  • 1
    @SergeyKalinichenko There's no requirement that a dupe has to absolutely answer the exact question as asked. If the dupe puts the asker on the path to solving their specific problem, it's done its job. Commented Jul 22, 2021 at 19:00
  • @PeterDuniho Thanks for your answer. I have edited the question and added some images to illustrate my desired result a bit better. Essentially, what I have done works, but I can't help thinking that there must be a nicer, cleaner, better way of achieving this - I just don't know it yet. Please review it if you have few minutes and please point me in the right direction, I will be very grateful. Commented Jul 22, 2021 at 19:05

2 Answers 2

2

A way to combine a static behaviour with an interface is to create a singleton class. Declare a settings interface:

public interface ISmsCommunicationSettings { public string[] SettingsKeys { get; } public string CommunicationProviderCode { get; } public string CommunicationProviderName { get; } public CommunicationType CommunicationProviderType { get; } } 

Implement a class and make it a singleton by hiding the constructor and provide a unique instance through a static field:

public class SMSCommunicationSettings : ISmsCommunicationSettings { public static readonly SMSCommunicationSettings Instance = new SMSCommunicationSettings(); private SMSCommunicationSettings() // Hide constructor {} public string[] SettingsKeys { get; } = { "AccountSID", "AuthToken", "SMSNumber"}; public string CommunicationProviderCode { get; } = "TwilioSMSProvider"; public string CommunicationProviderName { get; } = "Twilio SMS Provider"; public CommunicationType CommunicationProviderType { get; } = CommunicationType.SMS; } 

Add the settings to the provider interface:

public interface ICommunicationProvider { ISmsCommunicationSettings Settings { get; } CommunicationProvider CommunicationProvider { get; } } 

Implement the provider and pass the settings through constructor injection or reference it directly or get it through a factory method, etc.:

public class TwilioSMSCommunicationProvider : ISMSCommunicationProvider { ISmsCommunicationSettings Settings => SMSCommunicationSettings.Instance; CommunicationProvider CommunicationProvider { get; } } 

Now, every instance gets a reference to the same set of unique settings. No reflection required.


As an alternative to this Settings property, you could specify the name of the settings class in an attribute:

[CommSettings(nameof(SMSCommunicationSettings))] public class TwilioSMSCommunicationProvider : ISMSCommunicationProvider { ... } 

and then access the settings through reflection.


This works now in the latest C# 10 preview (you need Visual Studio 2022 preview):

public interface ISettings { } public interface ISettingProvider { static abstract ISettings Settings { get; } } public class Provider : ISettingProvider { public static ISettings Settings => throw new NotImplementedException(); } 
Sign up to request clarification or add additional context in comments.

5 Comments

Instead of adding the settings to ICommunicationProvider (which is a wrapper for other interfaces, email, post, whatsapp) can I add ISMSCommunicationSettings to ISMSCommunicationProvider instead? I don't want the SMS settings to be present in for example Email implementation.
I think the issue with this is that I need those settings (static fields) to be visible before the TwilioSMSCommunicationProvider class is instantiated. I fetch the Type using reflection, then I use GetField("settingsKeys").GetValue(null) to grab the values of those fields. I don't think I can do this with the properties, unless I create an instance of the implementation :(
You can access the settings before creating the provider through the static field SMSCommunicationSettings.Instance. Additionally, you could add a static property public static ISmsCommunicationSettings Settings => SMSCommunicationSettings.Instance; to the provider or specify the name of this class in an attribute. Several providers can return either the same settings or different settings and the settings are strongly typed through an interface and therefore easy to access. Only this Settings or Instance property would have to be accessed through reflection.
I added public static ISmsCommunicationSettings Settings => SMSCommunicationSettings.Instance; field to the provider, as this is the only way I can get it to work - but that means that having it declared on the Interface is pointless as Interface can't enforce the static field. It's still much cleaner, but I am just manually adding static field with Settings to each of the providers, and I have to remember about it rather than it being forced by the interface. If Settings is not static, I can't access it from the Type with reflection unless I create an instance.
Meanwhile the latest C# 10 preview (you need Visual Studio 2022 preview) features Static abstract members in interfaces.
2

Since the objective of this exercise is to deliver some metadata for the use in the UI code, you could use custom attributes to capture the desired information without the use of static fields:

[AttributeUsage(AttributeTargets.Class)] class CommunicationProviderMetadataAttribute : Attribute { public string ProviderCode { get; set; } public string ProviderName { get; set; } public CommunicationType CommunicationType { get; set; } public string[] SettingsKeys { get; set; } } 

Now you can decorate your classes with the attributes, rather than supplying the static data:

[CommunicationProviderMetadata( ProviderCode = "TwilioSMSProvider", ProviderName = "Twilio SMS Provider", CommunicationType = CommunicationType.SMS, SettingsKeys = { "AccountSID", "AuthToken", "SMSNumber"} )] public class TwilioSMSCommunicationProvider : ISMSCommunicationProvider { ... } 

This looks cleaner, and conveys the idea that it's metadata in the most explicit way.

Although you could still forget to add the attribute, there is a way to force the user to specify all of the parameters at compile time by adding a constructor that takes all four values. An advantage of this approach is that if you later decide to add a fifth parameter, the compiler would tell you all the spots where it is still missing.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.