如何在运行时修改IQueryable返回的内容?

时间:2018-07-10 03:07:30

标签: c#

我看不到如何在IQueryable返回的实体中设置值。

所以以下面的代码为例:

public IQueryable<CustomerInfo> CustomerInfoAsQueryable()
{
    IQueryable<CustomerInfo> customerInfo = _dbCustomerService.Info("CustomerID123");

    // Note: customerInfo.Logbook has value "~/dir/logbook.txt"

    // How to set customerInfo.LogbookContent to the contents of "~/dir/logbook.txt"

    return customerInfoAsQueryable;
}

现在,我必须将CustomerInfoAsQueryable传递给第三方控件,以便他们可以执行查询。我希望能够读取customerInfo.Logbook并找到文件,然后读取文件的内容,最后将customerInfo.LogbookContent设置为此内容。

我想这是一种延迟加载。

我该怎么做才能使第三方控制在向不同客户进行查询时获得customerInfo.LogbookContent

(注意:如何读取文件不是问题的主题)

1 个答案:

答案 0 :(得分:0)

在回答您的问题之前,我会警告您有关从considered a bad practice存储库中返回IQueryable的做法,因为它鼓励在应用程序层中分布的业务逻辑。 最好使用过滤器并返回只读的过滤结果。

然后关于CustomerInfo.LogbookContent的事情使我感到困扰。类CustomerInfo应该反映某些数据库中的表结构。因此,使用该字段会产生误导:人们认为该信息存储在数据库中,尽管绝对不是这种情况。另外,CustomerInfo.Logbook有一些冗余,它指定了一条路径。如何知道信息在哪里?因此,我认为删除CustomerInfo.LogbookContent会更好。

还有另一件事让我感到困扰:一些文件路径从持久性层(在CustomerInfo.Logbook中泄漏出来了,我将尽一切可能避免这种情况。您可以查看this video来了解为什么尽可能多地限制API是个好主意(总之,这会带来更好的设计和安全性)。

基于这些评论,我得出的结论是,您需要基于CustomerInfo的新的抽象级别,以便更好地为您的用户服务。

我会提出这样的设计:

public IReadOnlyList<Customer> GetCustomers(/*some filters (id = ..., name like ..., etc.)*/)
{
    return _dbCustomerService.Info("CustomerID123")
                             .Where(/*some filters (id = ..., name like ..., etc.)*/)
                             .Select(ci => new Customer(ci.Name, GetLogbookContent(ci.Logbook))
                             .ToList();
}

private string GetLogbookContent(string localFilePath)
{
    //Connect to AWS/Azure
    //Return file's content
}

public sealed class Customer
{
    public Customer(string name, string logbook)
    {
        Name = name;
        Logbook = logbook;
    }
    public string Name { get; }
    public string Logbook { get; }
}

有关此代码的一些注意事项:

  • 延迟加载日志的内容。另一方面,仅为相关的Customer加载。我认为这是一种正确的做法,因为如果您的用户想要这些客户,则应遵循最小惊讶原则并获取数据以避免以后再进行长途调用,因为这可能会引起问题,因为这是不可预期的(UI冻结等) )。
  • 我添加了一个Customer类,该类仅包含与您的用户有关的信息,并完全抽象了以下事实:数据既来自数据库又来自存储在某个云存储中的文件。
  • 我明确地返回一个IReadOnlyList来明确指定我们已经完成了所有持久性,并且结果现在已存储在内存中(否则可以在真正执行IQueryable之前关闭持久性层=>你好噩梦调试)。

现在,如果并且仅在调用GetCustomers时由于文件加载而花费的时间确实太长,仍然可以进行以下重构以启用日志的延迟加载:

public IReadOnlyList<Customer> Get(/*some filters (id = ..., name like ..., etc.)*/)
{
    return _dbCustomerService.Info("CustomerID123")
                             .Where(/*some filters (id = ..., name like ..., etc.)*/)
                             .Select(ci => new Customer(ci.Name, () => GetLogbookContent(ci.Logbook))
                             .ToList();
}

public sealed class Customer
{
    private string _logbook;
    private readonly Func<string> _loadLogbook;
    public Customer(string name, Func<string> logbook)
    {
        Name = name;
        _loadLogbook = logbook;
    }
    public string Name { get; }
    public string Logbook
    {
        get
        {
            if (_logbook == null)
            {
                _logbook = _loadLogbook();
            }
            return _logbook;
        }
    }
}

注意:这仅允许一次(第一次)从云存储中加载日志。但是,如果此文件的内容不断变化,并且您需要知道(=在每次调用Customer.Logbook时加载文件内容),则可以像这样重构Customer

public sealed class Customer
{
    private readonly Func<string> _loadLogbook;
    public Customer(string name, Func<string> logbook)
    {
        Name = name;
        _loadLogbook = logbook;
    }
    public string Name { get; }
    public string Logbook => _loadLogbook();
}

更新

根据您的评论,您不能绕过我最初做出的两个假设:

  • 您必须返回IQueryable
  • 您不能返回CustomerInfo以外的其他内容

基于此,我将实现惰性文件加载的方式(我假设CustomerInfo看起来像POCO类,对于某些ORM使用的类通常是这种情况):

public class CustomerInfo
{
    private Func<string, string> _loadLogbook;
    private string _logbookContent;
    ...
    public void SetLoadLogbook(Func<string, string> loadLogbook) => _loadLogbook = loadLogbook;
    ...
    public string Logbook { get; set; }
    public string LogbookContent
    {
        get
        {
            if(_logbookContent == null)
            {
                _logbookContent = _loadLogbook?.Invoke(Logbook);
            }
            return _logbookContent;
        }
        set => _logbookContent = value;
    }
    ...
}

然后CustomerInfoAsQueryable变成:

public IQueryable<CustomerInfo> CustomerInfoAsQueryable()
{
    return _dbCustomerService.Info("CustomerID123")
                             .Select(ci =>
                             {
                                 ci.SetLoadLogbook(path => GetLogbookContent(path));
                                 return ci;
                             });
}

注意:

  • 延迟加载行为不是通过设置器注入的,这就是我所说的“最后的注入”。实际上,setter注入比构造函数注入(可靠)要危险得多,因此,_loadLogbook?在未调用NullReferenceException的情况下保护代码不受SetLoadLogbook的侵害。
  • 通过引入具有副作用的块表达式(这使我可以注入行为),我不得不稍微滥用LINQ Select语句。

您知道此解决方案不是最优的,但是(应该)可以在约束条件下工作。