【问题标题】:C# Method to Compare DateTime Fields Specified During Execution?比较执行期间指定的日期时间字段的 C# 方法?
【发布时间】:2018-06-01 17:16:38
【问题描述】:

我的项目有许多带有日期字段的对象,我经常需要选择一个日期范围内的所有对象。

例如:

public class Contract
{
    public DateTime SignDate { get; set; }
    public DateTime ReleaseDate { get; set; }
}

public class PersonalCheck
{
    public DateTime SignDate { get; set; }
    public DateTime ProcessDate { get; set; }
    public DateTime VoidDate { get; set; }
}

如果我只关心 SignDate,那就很容易了。我会声明一个接口...

public interface IObjectWithSignDate
{
    DateTime SignDate { get; set; }
}

...更改我的其他对象以从它继承,然后创建一个这样的方法:

    public static IQueryable<T> SignedWithin<T>(this IQueryable<T> items, DateTime start, DateTime end) where T : IObjectWithSignDate
    {
        return items.Where(q => q.SignDate >= start && q.SignDate <= end);
    }

如何避免为 ReleaseDate、ProcessDate、VoidDate 等重写此函数?我可以让这个方法接受任何对象的 IQueryable 和一个告诉它运行这个选择器的日期字段的变量吗?

请注意,这必须能够 a) 在 LinqToEntities 中执行以针对数据库运行 b) 不会增加很多开销(因为我担心反射可能会这样做)

【问题讨论】:

  • 如果您添加Expression&lt;Func&lt;T, bool&gt;&gt; 的参数,您可以为您的where 子句传入谓词。您的调用者仍需要知道要比较的对象上的哪些日期,但它不会与特定的方法实现绑定。
  • 难道我还不需要为每个日期字段创建一个单独的谓词吗?
  • 来自呼叫站点,是的。您将不得不在某处指定日期字段。
  • 我的问题是,如果我为我的应用程序中的 6 个以上不同日期字段中的每一个创建一个谓词(它们都使用相同的比较规则),我需要对所有这些字段进行单元测试.这是我已经遇到的问题。我正在寻找一种方法将方法计数减至 1 以使其变得漂亮和干燥。
  • 嗯,你有 6+ 种不同的逻辑流程。它们可能相似,但它们都是不同的。如果您走表达式路线,您可以通过提供表达式和 IQueryable 作为测试方法的参数来使用一种方法测试您的案例,并通过一个方法调用所有内容,但这只是将多个单元测试的问题变成了多个测试案例。我不知道你会如何摆脱它,但我可能会遗漏一些东西。

标签: c# linq datetime generics compare


【解决方案1】:

简单但具体

你可以像这样添加一个扩展方法:

public static class DateTimeExtensions
{
    public static bool IsBetween(this DateTime thisDateTime, DateTime start, DateTime end)
    {
        return thisDateTime >= start && thisDateTime <= end;
    }
}

您可以单独进行单元测试。

然后,您可以在要检查的任何 DateTime 字段上使用它。例如:

var start = new DateTime(2017, 1, 1);
var end = new DateTime(2017, 12, 31, 23, 59, 59);
IList<Contract> contracts = new List<Contract>(); // or anything enumerable
var contractsSignedBetween = contracts.Where(x => x.SignDate.IsBetween(start, end));
var contractsReleasedBetween = contracts.Where(x => x.ReleaseDate.IsBetween(start, end));

(请注意我如何将开始日期时间设置为 00:00:00 时间,将结束日期时间设置为 23:59:59 时间[也可以包含毫秒],因此最后一天内的时间是包括在内。)

使其可重复使用

如果你发现自己需要做很多事情,你可以为此做一个扩展

public static class EnumerableContractsExtensions
{
    public static IEnumerable<Contract> SignedBetween(this IEnumerable<Contract> contracts, DateTime start, DateTime end)
    {
        return contracts.Where(x => x.SignDate.IsBetween(start, end));
    }
}

并像这样使用它

 var contractsSignedBetween = contracts.SignedBetween(start, end);

也可以单独进行单元测试。

更灵活但更具体

用表达式说出你想要的日期...

public static class EnumerableContractsExtensions
{
    public static IEnumerable<Contract> Between(this IEnumerable<Contract> contracts, Func<Contract, DateTime> selector, DateTime start, DateTime end)
    {
        return contracts.Where(x => selector(x).IsBetween(start, end));
    }
}

然后做:

var contractsSignedBetween = contracts.Between(x => x.SignDate, start, end);
var contractsReleasedBetween = contracts.Between(x => x.ReleaseDate, start, end);

灵活通用

全力以赴,通用地做(虽然你不能把它变成一个扩展方法,因为它是通用的):

public static class EnumerableExtensions
{
    public static IEnumerable<T> Between<T>(IEnumerable<T> items, Func<T, DateTime> selector, DateTime start, DateTime end)
    {
        return items.Where(x => selector(x).IsBetween(start, end));
    }
}

同样,这本身是可测试的,可以这样使用:

IList<Contract> contracts = new List<Contract>();
IList<PersonalCheck> personalChecks = new List<PersonalCheck>();
var contractsSignedBetween = EnumerableExtensions.Between(contracts, x => x.SignDate, start, end);
var checksSignedBetween = EnumerableExtensions.Between(personalChecks, x => x.SignDate, start, end);

使其可查询

要使其以 IQueryable 的形式工作,该方法需要转换为 expression tree,因为 LINQ to Entities 不知道如何将方法转换为 SQL。

public static IQueryable<TSource> Between<TSource, TKey>(
    this IQueryable<TSource> source,
    Expression<Func<TSource, TKey>> keySelector,
    TKey low,
    TKey high)
    where TKey : IComparable<TKey>
{
    Expression key = keySelector.Body;
    Expression lowerBound = Expression.LessThanOrEqual(Expression.Constant(low), key);
    Expression upperBound = Expression.LessThanOrEqual(key, Expression.Constant(high));
    Expression and = Expression.AndAlso(lowerBound, upperBound);
    Expression<Func<TSource, bool>> lambda =
        Expression.Lambda<Func<TSource, bool>>(and, keySelector.Parameters);
    return source.Where(lambda);
}

仍然会这样使用:

var contractsSignedBetween = contracts.Between(x => x.SignDate, start, end);

这也适用于DateTimes 以外的东西。希望这会有所帮助。

【讨论】:

  • 总结之前的讨论:感谢非常详细的示例! IEnumerable 查询在 Linq-to-SQL 中工作,但实际上在内存中执行。针对具有 10K+ 记录的数据库表,它必须在执行“Between”之前将它们全部加载,从而降低性能。最后的 IQueryable 部分对 SQL 不起作用——它会因“LINQ to Entities 不支持 LINQ 表达式节点类型‘Invoke’而崩溃。”
  • @JamesinIndy Aaargh。我把它复杂化了。唯一的变化是“关键”行。
  • 成功了!下周我将不得不做一些进一步的测试,以确保这将满足我的需求,但我相信它会。非常感谢!
  • 哇哦!这真是太有趣了,我们几乎一起努力跨时区解决问题,以及它导致在 my 代码中发现错误的扭曲。我喜欢这个网站。
  • 很高兴你也从中有所收获!进一步的测试看起来不错。我已将此标记为官方答案。感谢您的坚持!
猜你喜欢
  • 2020-12-16
  • 2012-01-20
  • 2014-09-28
  • 1970-01-01
  • 1970-01-01
  • 2010-09-11
  • 2013-10-28
  • 1970-01-01
  • 2010-11-06
相关资源
最近更新 更多