【问题标题】:Generic Import method for EFEF 的通用导入方法
【发布时间】:2019-06-10 15:25:49
【问题描述】:

我想做一个通用方法将数据导入我的应用程序。

例如,假设我有:

private static async Task<int> ImportAccount(string filename)
{
    var totalRecords = await GetLineCount(filename);
    var ctx = new AccountContext();
    var count = 0;
    var records = 0;
    using (var stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        using (var reader = new StreamReader(stream, Encoding.UTF8))
        {
            string line;
            while ((line = await reader.ReadLineAsync()) != null)
            {
                var data = line.Split('\t');
                var acc = new Account(data);
                await ctx.Accounts.AddAsync(acc);
                // need this to avoid using all the memory
                // maybe there is a smarter or beter way to do it
                // with 10k it uses about 500mb memory, 
                // files have million rows+
                if (count % 10000 == 1)
                {
                    records += result = await ctx.SaveChangesAsync();
                    if (result > 0)
                    {
                        ctx.Dispose();
                        ctx = new AccountContext();
                    }
                }
                count++;
            }
        }
    }
    await ctx.SaveChangesAsync();
    ctx.Dispose();
    return records;
}

在上面的示例中,我将数据从制表符分隔文件导入到 Accounts db。

然后我需要导入属性、土地和许多其他数据库。

不必像上面那样为每个数据库创建一个方法,我想做一些类似的东西:

internal static readonly Dictionary<string, ??> FilesToImport = new Dictionary<string, ??>
{
    { "fullpath to file", ?? would be what I need to pass to T }
    ... more files ...
};
private static async Task<int> Import<T>(string filename)

其中 T 是有问题的数据库。

我所有的类都有一个共同点,它们都有一个使用string[] data 的构造函数。

但我不知道如何制作我能够接受的方法:

private static async Task<int> Import<T>(string filename)

然后可以做一个:

var item = new T(data);
await ctx.Set<T>().AddAsync(item);

如果我没记错的话,我将无法用参数实例化 T。

我如何制作这个通用的 Import 方法,是否可以实现?

【问题讨论】:

    标签: c# entity-framework-core


    【解决方案1】:

    实现这一点的最简单方法是传递一个通用函数,该函数接受字符串行或拆分值的字符串数组,并返回一个设置了值的对象。使用支持泛型的ctx.AddAsync() 方法并将实体添加到正确的集合中。

    private static async Task<int> Import<T>(string filename, Func<string, T> transform) where T : class
    {
        var totalRecords = await GetLineCount(filename);
        var ctx = new AccountContext();
        var count = 0;
        var records = 0;
        using (var stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            using (var reader = new StreamReader(stream, Encoding.UTF8))
            {
                string line;
                while ((line = await reader.ReadLineAsync()) != null)
                {
                    var data = line.Split("\t");
                    var entity = transform(data);
                    await ctx.AddAsync(entity);
                    if (count % 10000 == 1)
                    {
                        records += result = await ctx.SaveChangesAsync();
                        if (result > 0)
                        {
                            ctx.Dispose();
                            ctx = new AccountContext();
                        }
                    }
                    count++;
                }
            }
        }
        await ctx.SaveChangesAsync();
        ctx.Dispose();
        return records;
    }
    
    // Usage
    
    Import(filename, splits => {
       / * do whatever you need to transform the data */
       return new Whatever(splits);
    })
    

    由于无法通过传递参数来构造泛型类型,因此您必须使用函数作为字典中的第二种类型。

    Dictionary<string, Func<string, object>> FilesToImport = new Dictionary<string, Func<string, object>>{
      { "fullpath to file", data => new Account(data) },
      { "fullpath to file", data => new Whatever(data) },
      { "fullpath to file", data => new Whatever2(data) },
    }
    

    【讨论】:

    • 这看起来很有趣但是var entity = transform(line)说它不能将字符串转换为T,
    • 抱歉,已编辑。将签名更改为Func&lt;string, T&gt;
    • 现在显示 The type 'T' must be a reference type in order to use it as parameter 'TEntity' in the generic type or method 'DbContext.AddAsync&lt;TEntity&gt;(TEntity, CancellationToken)' 但转换有效。
    • 如果我添加 where T : class 它会消失,但我不完全确定这是对的。
    • 但是我如何从字典中的类型创建一个新的 T 呢?否则我仍然需要自己为每个导入定义类型 return new Account(data); 应该是 return new T(data); 或类似的东西。
    【解决方案2】:

    C# 对泛型类型参数只有 new() 限制。但不幸的是,不可能强制一个类型有一个带参数的构造函数。

    一种解决方法是定义这样的接口:

    interface IImportedEntity<T>
    // where T: YourBaseClass
    {
        T Init(string[] data);
    }
    

    在这种情况下,所有实现类都必须实现这样的方法:

    class Account : /*YourBaseClass*/ IImportedEntity<Account>
    {
        public Account()
        {
            // for EF
        }
    
        // can be made private or protected
        public Account(string[] data)
        {
            // your code
        }
    
    
        // public Account Init(string[] data) => { /*populate current instance*/  return this;};
        // can be implemented in base class
        public Account Init(string[] data) => new Account(data);
    }
    

    最后你可以限制你的通用 Import 方法只处理导入的实体:

    private static async Task<int> Import<T>(string filename) 
          where T: class, IImportedEntity<T>, new()
    {
        ....
        var item = new T();
        item = item.Init(data);
        await ctx.Set<T>().AddAsync(item);
        ...
    }
    

    注意,如果您仍想将其与字典一起使用,则需要使用反射 (example)。

    【讨论】:

    • 这看起来很有希望,但我在 item = item.Init(data);await ctx.Set&lt;T&gt;().AddAsync(item); 的相应行上得到了 Cannot implicitly convert type 'V' to 'T'The type 'T' must be a reference type in order to use it as parameter 'TEntity' in the generic type or method 'DbContext.Set&lt;TEntity&gt;()'
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-06-23
    • 1970-01-01
    • 2016-04-26
    • 2022-01-09
    • 2012-08-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多