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?

Varin
  • 2,354
  • 2
  • 20
  • 37
  • 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 ... – Peter Duniho Jul 22 '21 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. – Peter Duniho Jul 22 '21 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). – Sergey Kalinichenko Jul 22 '21 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. – Ian Kemp Jul 22 '21 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. – Varin Jul 22 '21 at 19:05
  • @IanKemp Well, at least the duplicate should be in the same ballpark, and in my opinion the previously suggested duplicate wasn't close enough to this question. To continue with the analogy, it was the same sport, but it wasn't in the same ballpark. – Sergey Kalinichenko Jul 22 '21 at 19:09
  • I don't know why you are complicating things so much when all you need is a single `IMessageChannel` or similar. You even have interfaces for `ISms`, really? That is supposed to be a simple poco and you have different types of "sms" now? first do something straightforward, without any interfaces or classes, then try to identify common parts and extract them. Don't make everything an interface first and try to implement it later. – Mat J Jul 22 '21 at 19:12
  • Have you considered using Custom Attributes rather than static fields? It doesn't solve the problem of forcing people to add the information any time they implement the interface, but it feels like a better fit for the paradigm you're describing. – StriplingWarrior Jul 22 '21 at 19:32
  • "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." Souds like duplicating code. And duplicating code sounds like a missing 1 to many relationship. – Klamsi Jul 23 '21 at 06:31

2 Answers2

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.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
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();    
}
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • 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. – Varin Jul 22 '21 at 20:01
  • 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 :( – Varin Jul 22 '21 at 20:39
  • 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. – Olivier Jacot-Descombes Jul 23 '21 at 12:49
  • 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. – Varin Jul 24 '21 at 20:58
  • 1
    Meanwhile the latest C# 10 preview (you need Visual Studio 2022 preview) features [Static abstract members in interfaces](https://github.com/dotnet/csharplang/issues/4436). – Olivier Jacot-Descombes Jul 25 '21 at 14:59