Adding multiple data importers support to web applications
- by DigiMortal
I’m building web application for customer and there is requirement that users must be able to import data in different formats. Today we will support XLSX and ODF as import formats and some other formats are waiting. I wanted to be able to add new importers on the fly so I don’t have to deploy web application again when I add new importer or change some existing one. In this posting I will show you how to build generic importers support to your web application. Importer interface All importers we use must have something in common so we can easily detect them. To keep things simple I will use interface here. public interface IMyImporter { string[] SupportedFileExtensions { get; } ImportResult Import(Stream fileStream, string fileExtension); } Our interface has the following members: SupportedFileExtensions – string array of file extensions that importer supports. This property helps us find out what import formats are available and which importer to use with given format. Import – method that does the actual importing work. Besides file we give in as stream we also give file extension so importer can decide how to handle the file. It is enough to get started. When building real importers I am sure you will switch over to abstract base class. Importer class Here is sample importer that imports data from Excel and Word documents. Importer class with no implementation details looks like this: public class MyOpenXmlImporter : IMyImporter { public string[] SupportedFileExtensions { get { return new[] { "xlsx", "docx" }; } } public ImportResult Import(Stream fileStream, string extension) { // ... } } Finding supported import formats in web application Now we have importers created and it’s time to add them to web application. Usually we have one page or ASP.NET MVC controller where we need importers. To this page or controller we add the following method that uses reflection to find all classes that implement our IMyImporter interface. private static string[] GetImporterFileExtensions() { var types = from a in AppDomain.CurrentDomain.GetAssemblies() from t in a.GetTypes() where t.GetInterfaces().Contains(typeof(IMyImporter)) select t; var extensions = new Collection<string>(); foreach (var type in types) { var instance = (IMyImporter)type.InvokeMember(null, BindingFlags.CreateInstance, null, null, null); foreach (var extension in instance.SupportedFileExtensions) { if (extensions.Contains(extension)) continue; extensions.Add(extension); } } return extensions.ToArray(); } This code doesn’t look nice and is far from optimal but it works for us now. It is possible to improve performance of web application if we cache extensions and their corresponding types to some static dictionary. We have to fill it only once because our application is restarted when something changes in bin folder. Finding importer by extension When user uploads file we need to detect the extension of file and find the importer that supports given extension. We add another method to our page or controller that uses reflection to return us importer instance or null if extension is not supported. private static IMyImporter GetImporterForExtension(string extensionToFind) { var types = from a in AppDomain.CurrentDomain.GetAssemblies() from t in a.GetTypes() where t.GetInterfaces().Contains(typeof(IMyImporter)) select t; foreach (var type in types) { var instance = (IMyImporter)type.InvokeMember(null, BindingFlags.CreateInstance, null, null, null); if (instance.SupportedFileExtensions.Contains(extensionToFind)) { return instance; } } return null; } Here is example ASP.NET MVC controller action that accepts uploaded file, finds importer that can handle file and imports data. Again, this is sample code I kept minimal to better illustrate how things work. public ActionResult Import(MyImporterModel model) { var file = Request.Files[0]; var extension = Path.GetExtension(file.FileName).ToLower(); var importer = GetImporterForExtension(extension.Substring(1)); var result = importer.Import(file.InputStream, extension); if (result.Errors.Count > 0) { foreach (var error in result.Errors) ModelState.AddModelError("file", error); return Import(); } return RedirectToAction("Index"); } Conclusion That’s it. Using couple of ugly methods and one simple interface we were able to add importers support to our web application. Example code here is not perfect but it works. It is possible to cache mappings between file extensions and importer types to some static variable because changing of these mappings means that something is changed in bin folder of web application and web application is restarted in this case anyway.