Introduction
Sometimes, when writing C# code, there are situations when it would be great to write something like this:
public class Generic<TType> where TType : new(string, string, int)
{
...
var instance = new TType("Hello", "World", 15);
...
}
or this:
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...
where TGenericParameter : new(InitializationParameters)
...it provides...
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
:
...
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
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:
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
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:
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
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:
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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.