Extending DbProviderFactory for dynamic DataProvider loading

Mar 8, 2016 at 1:25 PM
Edited Mar 8, 2016 at 1:27 PM
Hey, I'm using this library for one of our project. Especially the Ado information extraction out of existing databases is on oft main used feature. Currently we're using the source code directly and extend it for a wrapper around DbProviderFactory. One requirement is dynamic extension of dataprovider dll without rebuild. Therefore we have a directory with dataprovider dll (and needed resources) which will be loaded at startup of application. Because DbProviderFactory needs registration in app.config (or in user or machine config) this won't work. Is there a possibility to extend your source code with this feature? Using Func property for determing DbProviderFactory will be enough. Or you could use our code (see below). I'm not familiar with forks and pull requests therefore the code will be inserted here:
    /// <summary>
    ///     Extension of DbProviderFactories for allowing programmatically adding external dll dataprovider which are not
    ///     declared at app.config or machine.config. Basically extracted from
    ///     http://sandrinodimattia.net/dbproviderfactoryrepository-managing-dbproviderfactories-in-code/
    /// </summary>
    public static class DbProviderFactoryRepository
    {
        #region Fields

        /// <summary>
        ///     The table containing all the data.
        /// </summary>
        private static DataTable _dbProviderFactoryTable;

        #endregion

        #region Constructors

        /// <summary>
        ///     Initialize the repository.
        /// </summary>
        static DbProviderFactoryRepository()
        {
            LoadDbProviderFactories();
        }

        #endregion

        #region Methods

        /// <summary>
        ///     Gets all providers.
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<DbProviderFactoryDescription> GetAllDescriptions()
        {
            return _dbProviderFactoryTable.Rows.Cast<DataRow>().Select(o => new DbProviderFactoryDescription(o));
        }

        /// <summary>
        ///     Get provider by invariant.
        /// </summary>
        /// <param name="invariant"></param>
        /// <returns></returns>
        public static DbProviderFactoryDescription GetDescriptionByInvariant(string invariant)
        {
            var row =
                _dbProviderFactoryTable.Rows.Cast<DataRow>()
                    .FirstOrDefault(o => o["InvariantName"] != null && o["InvariantName"].ToString() == invariant);
            return row != null ? new DbProviderFactoryDescription(row) : null;
        }

        /// <summary>
        ///     Gets the factory.
        /// </summary>
        /// <param name="description">The description.</param>
        /// <returns></returns>
        public static DbProviderFactory GetFactory(DbProviderFactoryDescription description)
        {
            DbProviderFactory dbFactory = null;

            var providerType = AssemblyHelper.LoadTypeFrom(description.AssemblyQualifiedName);
            var providerInstance = providerType?.GetField("Instance", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
            if (providerInstance != null)
            {
                if (providerInstance.FieldType.IsSubclassOf(typeof(DbProviderFactory)))
                {
                    var factory = providerInstance.GetValue(null);
                    if (null != factory)
                    {
                        dbFactory = (DbProviderFactory)factory;
                    }
                }
            }

            return dbFactory;
        }

        /// <summary>
        ///     Gets the factory.
        /// </summary>
        /// <param name="invariant">The invariant.</param>
        /// <returns></returns>
        public static DbProviderFactory GetFactory(string invariant)
        {
            if (string.IsNullOrEmpty(invariant))
            {
                throw new ArgumentNullException(nameof(invariant));
            }

            var desc = GetDescriptionByInvariant(invariant);
            return desc != null ? GetFactory(desc) : null;
        }

        /// <summary>
        ///     Loads the external database provider assemblies.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="includeSubfolders">if set to <c>true</c> [include subfolders].</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="System.ArgumentException">$Path does not {path} exist.</exception>
        public static void LoadExternalDbProviderAssemblies(string path, bool includeSubfolders = true)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException(nameof(path));
            }

            if (!Directory.Exists(path))
            {
                throw new ArgumentException($"Path does not {path} exist.", nameof(path));
            }

            // main directory
            var mainDirectory = new DirectoryInfo(path);
            var directories = new List<DirectoryInfo> { mainDirectory };

            // also search in direct subfolders
            if (includeSubfolders)
            {
                directories.AddRange(mainDirectory.GetDirectories());
            }

            // iterate over all directories and search for dll libraries
            foreach (var directory in directories)
            {
                foreach (var file in directory.GetFiles().Where(file => file.Extension.ToLower() == ".dll"))
                {
                    // This will work to load only the file from other directory without dependencies! But at access time the dependecies are necessary!
                    //var assembly = Assembly.LoadFile(file.FullName);

                    // Load all assemblies from directory in current AppDomain. This is necessary for accessing all types. Other
                    // opertunities like Assembly.LoadFile will only load one file temporary (later access will not have dependecy finding)
                    // and Assembly.ReflectionOnlyLoad will load all dependencies at beginning what will not work in other directories as bin.
                    var assemblyName = AssemblyName.GetAssemblyName(file.FullName);
                    var assembly = AppDomain.CurrentDomain.Load(assemblyName);
                    foreach (var type in assembly.GetLoadableTypes())
                    {
                        if (type.IsClass)
                        {
                            if (typeof(DbProviderFactory).IsAssignableFrom(type))
                            {
                                // Ignore already existing provider
                                if (GetDescriptionByInvariant(type.Namespace) == null)
                                {
                                    var newDescription = new DbProviderFactoryDescription
                                    {
                                        Description = ".Net Framework Data Provider for " + type.Name,
                                        InvariantName = type.Namespace,
                                        Name = type.Name + " Data Provider",
                                        AssemblyQualifiedName = type.AssemblyQualifiedName
                                    };
                                    Add(newDescription);
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        ///     Adds the specified provider.
        /// </summary>
        /// <param name="provider">The provider.</param>
        private static void Add(DbProviderFactoryDescription provider)
        {
            Delete(provider);
            _dbProviderFactoryTable.Rows.Add(provider.Name, provider.Description, provider.InvariantName, provider.AssemblyQualifiedName);
        }

        /// <summary>
        ///     Deletes the specified provider if present.
        /// </summary>
        /// <param name="provider">The provider.</param>
        private static void Delete(DbProviderFactoryDescription provider)
        {
            var row =
                _dbProviderFactoryTable.Rows.Cast<DataRow>()
                    .FirstOrDefault(o => o["InvariantName"] != null && o["InvariantName"].ToString() == provider.InvariantName);
            if (row != null)
            {
                _dbProviderFactoryTable.Rows.Remove(row);
            }
        }

        /// <summary>
        ///     Opens the table.
        /// </summary>
        private static void LoadDbProviderFactories()
        {
            _dbProviderFactoryTable = DbProviderFactories.GetFactoryClasses();
        }
#endregion
}
Mar 8, 2016 at 1:25 PM
    /// <summary>
    ///     Description of a DbProviderFactory for Repository.
    /// </summary>
    public class DbProviderFactoryDescription
    {
        #region Constructors

        /// <summary>
        ///     Initialize the description.
        /// </summary>
        public DbProviderFactoryDescription() {}

        /// <summary>
        ///     Initialize the description.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="description"></param>
        /// <param name="invariant"></param>
        /// <param name="type"></param>
        public DbProviderFactoryDescription(string name, string description, string invariant, string type)
        {
            Name = name;
            Description = description;
            InvariantName = invariant;
            AssemblyQualifiedName = type;
        }

        /// <summary>
        ///     Initialize the description based on a row.
        /// </summary>
        /// <param name="row">The row.</param>
        internal DbProviderFactoryDescription(DataRow row)
        {
            Name = row["Name"]?.ToString();
            Description = row["Description"]?.ToString();
            InvariantName = row["InvariantName"]?.ToString();
            AssemblyQualifiedName = row["AssemblyQualifiedName"]?.ToString();
        }

        #endregion

        #region Properties

        /// <summary>
        ///     Gets or sets the assemblyQualifiedName.
        /// </summary>
        /// <value>The assemblyQualifiedName.</value>
        public string AssemblyQualifiedName { get; set; }

        /// <summary>
        ///     Gets or sets the description.
        /// </summary>
        /// <value>The description.</value>
        public string Description { get; set; }

        /// <summary>
        ///     Gets or sets the invariantName.
        /// </summary>
        /// <value>The invariantName.</value>
        public string InvariantName { get; set; }

        /// <summary>
        ///     Gets or sets the name.
        /// </summary>
        /// <value>The name.</value>
        public string Name { get; set; }

        #endregion
    }
Coordinator
Mar 9, 2016 at 6:19 PM
That's a nice way to get providers registered. I've added it into DatabaseSchemaReader.Utilities, with some minor changes.
You can download the latest source from the source-code tab.

Thanks!
Mar 10, 2016 at 12:13 PM
Thanks for extending your library. For using dynamic dataprovider loading it is necessary to replace each DbProviderFactories.GetFactory() access with DbProviderFactoryRepository.GetFactory(). Otherwise dynamic loading will not work with f.e. SchemaReader. In case dynamic loading mechanismn is not used the behaviour should be the same.
Coordinator
Mar 10, 2016 at 6:28 PM
Like this?
FactoryTools.ProviderRepository = new DbProviderFactoryRepository();
var manualDescription = new DbProviderFactoryDescription
{
 Description = ".NET Framework Data Provider for SuperDuperDatabase",
 InvariantName = "SuperDuperDatabase",
 Name = "SuperDuperDatabase Data Provider",
 AssemblyQualifiedName = typeof(SuperDuperProviderFactory).AssemblyQualifiedName,
};
FactoryTools.ProviderRepository.Add(manualDescription);

var dr = new DatabaseReader(ConnectionStrings.Northwind, "SuperDuperDatabase");
var schema = dr.ReadAll();
I made DbProviderFactoryRepository non-static (like Sandrino's original repository), and hooked it into the existing static FactoryTools.
Mar 14, 2016 at 4:00 PM
I think this will work. There exists some other points where DbProviderFactories.GetFactory() is directly used and could be changed to FactoryTools.GetFactory(), but for my purposes it should work. Could you publish it to nuget please?
Coordinator
Mar 14, 2016 at 7:47 PM
Pushed to nuget. Enjoy!
Mar 14, 2016 at 8:09 PM
Thanks a lot!