首页 > 解决方案 > 如何修改 IQueryable 在运行时返回的内容?

问题描述

如果可能的话,我看不到如何在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给第 3 方控件,以便他们可以进行查询。我希望能够读取customerInfo.Logbook并找到该文件,然后读取最终设置customerInfo.LogbookContent为此内容的文件内容。

我想这是一种延迟加载。

customerInfo.LogbookContent当他们为不同的客户进行查询时,我该如何执行此操作以允许第 3 方控件获得?

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

标签: c#

解决方案


在我回答你的问题之前,我会警告你IQueryable从存储库返回一个被认为是不好的做法的做法,因为它鼓励业务逻辑跨应用程序层传播。最好采用过滤器并返回只读过滤结果。

然后有些事情困扰着我CustomerInfo.LogbookContent。该类CustomerInfo应该反映某些数据库中的表结构。因此,拥有该字段是一种误导:人们认为该信息存储在数据库中,但事实并非如此。另外,有一些冗余CustomerInfo.Logbook用于指定路径。如何知道信息在哪里?这就是我认为最好删除的原因CustomerInfo.LogbookContent

还有一件事困扰着我:一些文件路径从你的持久层(in CustomerInfo.Logbook)中泄漏出来,我会尽我所能避免这种情况。您可以观看此视频以了解为什么尽可能限制您的 API 是一个好主意(简而言之,它会带来更好的设计和安全性)。

基于这些评论,我得出的结论是,您需要基于 top of 的新抽象级别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仅包含与您的用户相关的信息的类,并完全抽象出数据来自数据库和存储在某些云存储上的文件这一事实
  • 我明确返回 anIReadOnlyList以明确说明我们已完成任何持久性并且结果现在在内存中(否则持久层可以在IQueryable真正执行之前关闭 => hello nightmare 调试)。

现在,当且仅当调用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 注入的,我称之为“最后的注入”。实际上,setter 注入比构造函数注入(可靠)更危险,因此不会调用_loadLogbook?保护您的代码免受NullReferenceException万一的情况。SetLoadLogbook
  • 我被迫通过引入具有副作用的块表达式(它允许我注入行为)来稍微滥用 LINQ Select 语句。

如您所知,此解决方案不是最理想的,但(应该)使用您的约束。


推荐阅读