将 DateTime 作为字符串进行比较到 LINQ to Entities 无法识别 DateTime.parse(s

本文关键字:DateTime Entities 识别 parse to 字符串 比较 LINQ | 更新日期: 2023-09-27 18:33:57

我正在开发一个带有C#,.NET Framework 4.0和Entity Framework Code First的WCF RESTful服务。

我有这个类(代表数据库上的一个表(:

[DataContract]
public class PostLine
{
    public int PostLineId { get; set; }
    [DataMember]
    public int? UserId { get; set; }
    [DataMember]
    public string Description { get; set; }
    [DataMember]
    public string DateUtc { get; set; }
    public User Author { get; set; }
}

我正在尝试这样做:

DateTime fourDaysAgo = DateTime.Now.Date.AddDays(-4);
var postLines = 
    context.PostLines.Where(p => DateTime.Compare(DateTime.Parse(p.DateUtc), fourDaysAgo) > 0).Include("Author");

但是我收到以下错误:

{System.NotSupportedException: LINQ to Entities doesn't recognize the method 'System.DateTime Parse(System.String)', which can not be converted into an expression of the repository.

我需要该PostLine.DateUtc是一个字符串,因为我将在我的 Web 服务上使用它,并将其作为 JSON 发送,因此最好将其存储为字符串。

如果我使用DateTime类型,我将在 JSON 响应上得到类似的东西:

{
    "DateUtc": "/Date(1380924000000+0200)/",
    "Description": "post_1",
    "UserId": 1
}

您知道如何在 LINQ 表达式上将字符串与日期时间进行比较吗?

将 DateTime 作为字符串进行比较到 LINQ to Entities 无法识别 DateTime.parse(s

我认为最好的方法是将属性一分为二。

实体框架需要一个DateTime属性。这是完全有道理的。

对于序列化,您需要一个 string 属性。这也是完全有道理的。

但是,您尝试对两者使用单个属性,这没有意义,也不是必需的。

[DataContract]
public class PostLine
{
    ...
    public DateTime DateUtcAsDateTime { get; set; }
    [DataMember, NotMapped]
    public string DateUtcAsString {
        get { return DateUtcAsDateTime.ToString(); }
        set { DateUtcAsDateTime = DateTime.Parse(value); }
    }
    ...
}

现在,DateUtcAsDateTime将由实体框架使用,DateUtcAsString将被实体框架忽略,因为它具有NotMapped属性。

另一方面,DateUtcAsString 是这些属性中唯一具有 DataMember 属性的属性,因此应该是唯一被序列化的属性。

当然,如果需要,您可以将这些属性之一重命名回DateUtc

更新:正如 Matt Johnson 指出的那样,改进是以始终产生完全相同字符串的方式指定格式。这可确保字符串不会更改,因为代码被移动到另一台碰巧具有不同区域设置的服务器。

[DataMember, NotMapped]
public string DateUtcAsString {
    get { return DateUtcAsDateTime.ToString("o"); }
    set { DateUtcAsDateTime = DateTime.Parse(value, "o", null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); }
}

请注意,我使用的是DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal而不是他建议的DateTimeStyles.RoundTripKind,因为名称DateUtc强烈表明您始终需要UTC,而不是本地时间。而且我没有明确指定任何文化,因为"o"格式已经独立于文化。

如果其他代码更容易处理,则可以使用"r"而不是具有相同优势的"o"

如果我见过一个XY问题,这肯定是一个XY问题。 您询问的是将日期时间作为实体框架中的字符串进行比较,而真正的问题是您不喜欢 WCF 默认使用的DataContractJsonSerializer的默认日期格式。

部分问题在于您混合了本地和 UTC。 您得到的是/Date(1380924000000+0200)/,其中包含服务器的本地时区偏移量。 这是因为您从 DateTime.Now 开始,它的.KindDateTimeKind.Local

如果你改用DateTime.UtcNow,它的.Kind将是DateTimeKind.Utc,并且会被序列化为/Date(1380924000000)/。 是的,格式的数字部分将是相同的。 即使指定了偏移量,数字部分仍与 UTC 相关。

这只是这种格式的一个问题。 另一个是,虽然DataContractJsonSerializer在序列化期间写入本地偏移量,但在反序列化期间它没有正确使用它。 它只是假设如果提供了任何偏移量,则时间应该是本地的 - 即使执行反序列化的计算机具有完全不同的偏移量

在 JSON 中使用更好的格式是 ISO8601 格式。 例如,此 UTC 值看起来像 2013-10-04T22:00:00.000Z 。 虽然如果您直接使用它,您可以轻松地将不同的日期格式传递给DataContractJsonSerializer,但 WCF 不会轻易向您公开此格式。

您可以通过自定义 WCF 消息格式化程序更改DataContractJsonSerializer设置,如此处、此处和此处所述,但它很快就会变得复杂。 如果这样做要小心,一定要彻底测试!

另一个想法是编写一个使用 JSON.Net 而不是 DataContractJsonSerializer 的自定义 WCF 消息格式化程序。 默认情况下,JSON.Net 使用 ISO8601 格式,因此您将被设置。

但老实说,最好的解决方案是不要尝试使用 WCF 来生成 REST 终结点。 日期格式问题只是一个开始。 在此过程中还可能出现各种其他问题。 相反,请使用为此目的而设计的现代框架,例如 ASP.Net WebAPI 或 ServiceStack。

ASP.Net WebAPI 使用 JSON.Net,ServiceStack 有自己的 JSON 序列化程序,称为 ServiceStack.Text。 JSON.Net 使用ISO8601作为默认日期格式。 如果使用服务堆栈,则需要设置JsConfig.DateHandler = JsonDateHandler.ISO8601;

所以在回顾中,你有两个XY问题:

  • 首先,您选择了 WCF 来构建 rest 终结点,这非常成问题,以至于行业开发了其他解决方案。
  • 其次,无法让DateTime发出正确的格式,因此决定将其视为字符串,这样就无法将其与 EF 查询中的DateTime进行比较。

是的,我意识到我没有回答您关于如何将DateTime输入与实体框架中的字符串属性进行比较的问题,但我认为现在您可以明白原因了。 如果你真的想走这条路,你可能会在SqlFunctionsEntityFunctions中找到一些有用的东西 - 但即便如此,你将如何在数据库中的这个列上建立一个有效的索引? 如果您获得大量数据,查询将非常慢。 我建议不要这样做。

我不知道

你是如何实例化你的DataContractJsonSerializer的.如果您直接实例化它...您可以传递一个DataContractJsonSerializerSettings,其中包含用于设置DateTime序列化方式的DateTimeFormat

如果您使用行为来实例化序列化程序,事情会稍微复杂一些。

如果你真的想在将数据传输到客户端时为类使用字符串,你应该有一个单独的 DTO 类。

然后,可以使用 AutoMapper 等库将PostLine类映射到带有表达式的PostLineDto

但是,您将面临的下一个问题是您的Expression<Func<PostLine,PostLineDto>>将包含Expression.MethodCall(DateTime.Parse),并且您必须注入可以转换的ExpressionVisitor...

p => p.DateUtc > DateTime.Parse("2013 Aug 1") - bool

p => p.DateUtc > new DateTime(1, Aug, 2013) - bool

这是一个真正的痛苦。