Click here to Skip to main content
15,884,388 members
Articles / Programming Languages / C#

Static Interfaces in C#

Rate me:
Please Sign up or sign in to vote.
4.78/5 (5 votes)
22 Jun 2019CPOL3 min read 16.2K   8   1
Introducing a library (distributed as a Nuget package) that is compensating for the lack of static interfaces in C#

Introduction

Sometimes, when writing C# code, there are situations when it would be great to write something like this:

C#
public class Generic<TType> where TType : new(string, string, int)
{
...
  var instance = new TType("Hello", "World", 15);
...
}

or this:

C#
public interface IStaticInterface
{
  static string FetchData(string url);
}

public class GenericDataConsumer<TDataSource> where TDataSource : IStaticInterface
{
...
    var result = TDataDource.FetchData(url);
...
}

This is impossible to do in C#. The library StaticInterface provides a workaround for the first case and the following text describes some details. Instead of having...

C#
where TGenericParameter : new(InitializationParameters)

...it provides...

C#
where TGenericParameter : IConstructible<InitializationParameters>

...and factory class StaticInterfaceFactory<InitializationParameters>.

Motivation

Recently, I was developing a tool for parsing import files for the client. An import file is essentially a CSV with a list of entities. There can be eight basic import file types, and some composite - meaning one file actually contains two entities on the same line.

On the one hand, the import file types share some commonalities that are better to solve with a common code - storing the files, copying the files to the share, deleting the files. All this is working completely the same and one does not need to know what sort of file he is operating with. On the other hand, there are differences that require knowledge of file contents. Especially in the first part of the process, which is loading the data. For example, it is necessary to insert artificial GUIDs into the two parts of the composite import file to relate the two entities together.

In terms of the code, loading the import files looks like this: there is a file repository containing files of any of the types. At this point, the repository can store some sort of IEnumerable<ImportFileBase>, it does not care about the particular files.

Generic types can only have base classes and implemented interfaces as their constraints. Such constraints are non-static. With the only exception being the parameterless constructor, which is a static class. That is a springboard for our StaticInterface library. At this point, the library only provides a factory to create instances with given parameters. To relate this to the import files, let us assume that all the file types are initialized with the same parameters. This is what can be done with StaticInterface:

C#
...
      public static TImportFile Create<TImportFile>(string path, DateTime date, ...)
      {
        StaticInterface.StaticFactory.Create<TImportFile>(
          new ImportFileInitializationParameters
          {
            Path = path,
            Date = date
          });
      }
...

Using the Code

Step number one is downloading and referencing the StaticInterface Nuget package (https://www.nuget.org/packages/StaticInterface/). Then, there are these basic ways of using it:

Params Array Option

C#
public class XlsFile : IConstructible
{
  private bool _isReadOnly;
  private int _idColumnIndex;
  private string _destinationFolder;
  private IEnumerable<string> _whitelistedColumnNames;

  public XlsFile()
  {    
  }

  public void Initialize(params object[] parameters)
  {
    _isReadOnly = (bool)parameters[0];
    _idColumnIndex = (int)parameters[1];
    _destinationFolder = (string)parameters[2];
    _whitelistedColumnNames = (IEnumerable<string>)parameters[3];
  }
}

public class XlsxFile : IConstructible
{
  private bool _bool;
  private int _int;
  private string _string;
  private IEnumerable<string> _whitelistedColumnNames;

  public XlsxFile()
  {   
  }

  public void Initialize(params object[] parameters)
  {
    _isReadOnly = (bool)parameters[0];
    _idColumnIndex = (int)parameters[1];
    _destinationFolder = (string)parameters[2];
    _whitelistedColumnNames = (IEnumerable<string>)parameters[3];
  }
}

The factory method will then look like this:

C#
public IConstructible FromFile(
  string path,
  bool isReadOnly,
  int idColumnIndex,
  string destinationFolder,
  IEnumerable<string> whitelistedColumnNames)
{
  object[] @params = new object[]
  {
    isReadOnly,
    idColumnIndex,
    destinationFolder,
    whitelistedColumnNames
  };
  switch (Path.GetExtension(path))
  {
    case ".xls":
      return StaticInterfaceFactory.Create<XlsFile>(@params);
    case ".xlsx":
      return StaticInterfaceFactory.Create<XlsxFile>(@params);
    default:
      throw new ArgumentException("Unsupported file type");
  }
}

Parameter Class Option

C#
public class FileInitializationParameters
{
  public bool IsReadOnly { get; set; }
  public int IdColumnIndex { get; set; }
  public string DestinationFolder { get; set; }
  public IEnumerable<string> WhitelistedColumnNames { get; set; }
}

public class XlsFile : IConstructible<FileInitializationParameters>
{
  private FileInitializationParameters _params;

  public XlsFile()
  {    
  }

  public void Initialize(FileInitializationParameters @params)
  {
    _params = @params;
  }
}

public class XlsxFile : IConstructible<FileInitializationParameters>
{
  private FileInitializationParameters _params;

  public XlsxFile()
  {   
  }

  public void Initialize(FileInitializationParameters @params)
  {
    _params = @params;
  }
}

The factory method will then look like this:

C#
public IConstructible<FileInitializationParameters> FromFile(
  string path,
  bool isReadOnly,
  int idColumnIndex,
  string destinationFolder,
  IEnumerable<string> whitelistedColumnNames)
{
  FileInitializationParameters @params = new FileInitializationParameters
  {
    IsReadOnly = isReadOnly,
    IdColumnIndex = idColumnIndex,
    DestinationFolder = destinationFolder,
    WhitelistedColumnNames = whitelistedColumnNames
  };
  switch (Path.GetExtension(path))
  {
    case ".xls":
      return StaticInterfaceFactory<FileInitializationParameters>.Create<XlsFile>(@params);
    case ".xlsx":
      return StaticInterfaceFactory<FileInitializationParameters>.Create<XlsxFile>(@params);
    default:
      throw new ArgumentException("Unsupported file type");
  }

Both these usages still require an if-then-else or switch-case branching and breakdown per cases. Which might still be too much code. Let us see the neatest way of writing the code.

Parameter Class Option With Non-Generic Method Call

C#
public class FileInitializationParameters
{
  public bool IsReadOnly { get; set; }
  public int IdColumnIndex { get; set; }
  public string DestinationFolder { get; set; }
  public IEnumerable<string> WhitelistedColumnNames { get; set; }
}

public class XlsFile : IConstructible<FileInitializationParameters>
{
  private FileInitializationParameters _params;

  public XlsFile()
  {   
  }

  public void Initialize(FileInitializationParameters @params)
  {
    _params = @params;
  }
}

public class XlsxFile : IConstructible<FileInitializationParameters>
{
  private FileInitializationParameters _params;

  public XlsxFile()
  {   
  }

  public void Initialize(FileInitializationParameters @params)
  {
    _params = @params;
  }
}

The factory method will then look like this:

C#
private static readonly Dictionary<string, Type> ExtensionToTypeMapping = 
                                                   new Dictionary<string, Type>
{
  { ".xls", typeof(XlsFile) },
  { ".xlsx", typeof(XlsxFile) }
};
public IConstructible<FileInitializationParameters> FromFile(
  string path,
  bool isReadOnly,
  int idColumnIndex,
  string destinationFolder,
  IEnumerable<string> whitelistedColumnNames)
{
  FileInitializationParameters @params = new FileInitializationParameters
  {
    IsReadOnly = isReadOnly,
    IdColumnIndex = idColumnIndex,
    DestinationFolder = destinationFolder,
    WhitelistedColumnNames = whitelistedColumnNames
  };
  return StaticInterfaceFactory<FileInitializationParameters>.Create(
    ExtensionToTypeMapping[Path.GetExtension(path)],
    @params);
}

Conclusion

Big projects have dependency injection containers. There often are situations, however, when you need to choose from multiple classes with the same input parameters but introducing a DI container would be too costly, perhaps also ineffective. This is the case Static Interface library is addressing - lightweight applications with tasks where multiple classes with same initialization parameters are needed to provide diverse implementations for various types of situations of the same problem.

History

  • 22nd June, 2019 - First version of the article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Czech Republic Czech Republic
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Generalwhere is demo code? Pin
Southmountain22-Jun-19 10:19
Southmountain22-Jun-19 10:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.