【问题标题】:Passing DateTimeOffset as WebAPI query string将 DateTimeOffset 作为 WebAPI 查询字符串传递
【发布时间】:2015-04-17 10:54:27
【问题描述】:

我有一个如下所示的 WebAPI 操作:

[Route("api/values/{id}")]
public async Task<HttpResponseMessage> Delete(string id, DateTimeOffset date) {
    //do stuff
}

但是当我从 HttpClient 实例调用它时,会创建一个如下 URL:

string.Format("http://localhost:1234/api/values/1?date={0}", System.Net.WebUtility.UrlEncode(DateTimeOffset.Now.ToString()));
// -> "http://localhost:1234/api/values/1?date=17%2F02%2F2015+7%3A18%3A39+AM+%2B11%3A00"

我收到 400 回复,说不可为空的参数 date 不存在。

我也尝试将[FromUri] 属性添加到参数中,但它仍然无法映射。如果我将其更改为 DateTimeOffset? 我可以看到它保留为 null 并查看 Request.RequestUri.Query 值在那里,只是没有映射。

最后我尝试不做WebUtility.UrlEncode 并没有什么不同。

【问题讨论】:

    标签: c# .net asp.net-web-api


    【解决方案1】:

    回答

    要将DateTimeOffset 发送到您的API,请将其转换为UTC 后将其格式化为:

    2017-04-17T05:04:18.070Z

    完整的 API URL 如下所示:

    http://localhost:1234/api/values/1?date=2017-04-17T05:45:18.070Z

    首先将 DateTimeOffset 转换为 UTC 很重要,因为正如 @OffHeGoes 指出的 in the comments,字符串末尾的 Z 表示 Zulu Time(通常称为 UTC)。

    代码

    您可以使用.ToUniversalTime().ToString(yyyy-MM-ddTHH:mm:ss.fffZ) 来解析DateTimeOffset。

    为确保您的 DateTimeOffset 使用正确的时区进行格式化,请始终使用 .ToUniversalTime() 首先将 DateTimeOffset 值转换为 UTC,因为字符串末尾的 Z 表示 UTC,也就是“祖鲁时间”。

    DateTimeOffset currentTime = DateTimeOffset.UtcNow;
    string dateTimeOffsetAsAPIParameter = currentDateTimeOffset.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
    string apiUrl = string.Format("http://localhost:1234/api/values/1?date={0}", dateTimeOffsetAsAPIParameter);
    

    【讨论】:

    • ":" 是 Web uri 中的无效字符,请参阅:stackoverflow.com/questions/2366260/…
    • 请注意,格式化为yyyy-MM-ddTHH:mm:ss.fffZ 会强制将日期格式化为字符串没有时区偏移...在此示例中很好,因为值为@987654336 @ 但如果您使用非 UTC 值,则此格式将不代表实际日期。
    • 很好,@OffHeGoes!我更新了答案以感谢您的评论?
    • 可悲的是,尽管得到了很多支持(和多次编辑),但这个答案仍然是完全错误的。它丢弃了偏移量,正如人们可能得到的名称一样,它是 DateTimeOffset 的整个点。并且非常清楚,如果您使用此代码,一旦 DateTimeOffset 被反序列化,您将获得正确的时间,但不知道 UTC 与事件发生的时区之间的时差。
    【解决方案2】:

    当前接受的答案会丢弃时区信息,这在某些情况下很重要。以下保持时区并且不会丢失任何精度。在构建查询字符串时,它还可以使您的代码保持简洁。

    public static string UrlEncode(this DateTimeOffset dateTimeOffset)
    {
         return HttpUtility.UrlEncode(dateTimeOffset.ToString("o"));
    }
    

    【讨论】:

    • 这是绝对正确答案。其他答案没有解决a)偏移量应该是“Z”(对于UTC)或“+xx:xx”(对于非UTC)的想法,以及b)+xx:xx偏移量的问题是由于“+”而不是 url 友好
    【解决方案3】:

    问题是偏移部分的 +(加号) 字符,我们应该对其进行编码。

    如果偏移量是-(减号)不需要编码

    +(plus)的编码值为%2B 所以 2021-05-05T18:00:00+05:00 应该被传递为 2021-05-05T18:00:00%2B05:00

    http://localhost:1234/api/values/1?date=2021-05-05T18:00:00%2B05:00
    

    如果偏移量是-(减号)那么

    http://localhost:1234/api/values/1?date=2021-05-05T18:00:00-05:00
    

    【讨论】:

      【解决方案4】:

      400 响应消息准确地描述了问题,尽管它本来可以更清楚。由属性定义的路由只需要一个参数 id,但 Delete 方法需要另一个名为 date 的参数。

      如果您想使用查询字符串提供此值,则需要使用“DateTimeOffset?”使该参数为空,这也会将其转换为可选参数。如果日期是必填字段,请考虑将其添加到路线中,例如:

      [Route("api/values/{id}/{date}")]
      

      好的,忽略我上面输入的内容,这只是格式问题。 Web API 无法确定解析给定值所需的文化,但如果您尝试在查询字符串中使用 JSON 格式(如 2014-05-06T22:24:55Z)传递 DateTimeOffset,则应该可以。

      【讨论】:

      • 如何在日期时间中也指定 TimeZone
      • @ArjunMenon 忽略此答案并在下面查看我的答案。然后请大家帮个忙并点赞。
      【解决方案5】:

      为了实现这一点,我正在使用

      internal static class DateTimeOffsetExtensions
      {
          private const string Iso8601UtcDateTimeFormat = "yyyy-MM-ddTHH:mm:ssZ";
      
          public static string ToIso8601DateTimeOffset(this DateTimeOffset dateTimeOffset)
          {
              return dateTimeOffset.ToUniversalTime().ToString(Iso8601UtcDateTimeFormat);
          }
      }
      

      【讨论】:

      • :默认不是有效字符。
      • ":" 是 Web uri 中的无效字符,请参阅:stackoverflow.com/questions/2366260/…
      • 所以这意味着您只能将日期值作为 UTC 值传递给 API?似乎有点受限...您希望能够通过任何时区 - 这不是 ISO 8601 的好处之一吗?
      【解决方案6】:

      如下创建自定义类型转换器:

      public class DateTimeOffsetConverter : TypeConverter
      {
          public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
          {
              if (sourceType == typeof(string))
                  return true;
      
              return base.CanConvertFrom(context, sourceType);
          }
      
          public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
          {
              if (destinationType == typeof(DateTimeOffset))
                  return true;
      
              return base.CanConvertTo(context, destinationType);
          }
      
          public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
          {
              var s = value as string;
      
              if (s != null)
              {
                  if (s.EndsWith("Z", StringComparison.OrdinalIgnoreCase))
                  {
                      s = s.Substring(0, s.Length - 1) + "+0000";
                  }
      
                  DateTimeOffset result;
                  if (DateTimeOffset.TryParseExact(s, "yyyyMMdd'T'HHmmss.FFFFFFFzzz", CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
                  {
                      return result;
                  }
              }
      
              return base.ConvertFrom(context, culture, value);
          }
      

      在你的启动序列中,比如WebApiConfig.Register,动态添加这个类型转换器到DateTimeOffset结构中:

      TypeDescriptor.AddAttributes(typeof(DateTimeOffset),
                                   new TypeConverterAttribute(typeof(DateTimeOffsetConverter)));
      

      您现在可以只以 ISO8601 的紧凑形式传递 DateTimeOffset 值,它会省略干扰 URL 的连字符和冒号:

      api/values/20171231T012345-0530
      api/values/20171231T012345+0000
      api/values/20171231T012345Z
      

      请注意,如果您有小数秒,您可能需要include a trailing slash in the url

      api/values/20171231T012345.1234567-0530/
      

      如果你愿意,你也可以把它放在查询字符串中:

      api/values?foo=20171231T012345-0530
      

      【讨论】:

      • 我能够通过复制框架的源代码DateTimeOffsetConverter,然后添加input = Uri.UnescapeDataString(str);这一行来使这种方法发挥作用
      • 你不需要这样的转换器。客户端和服务器端已经为您提供了功能。请看我的回答stackoverflow.com/a/51011977/492482
      【解决方案7】:

      使用 ISO 8601 datetime format 说明符:

      $"http://localhost:1234/api/values/1?date={DateTime.UtcNow.ToString("o")}"
      

      $"http://localhost:1234/api/values/1?date={DateTime.UtcNow:o}"
      

      【讨论】:

      • 这在非 UTC 日期的情况下不起作用,因为偏移量中的“+”不是 URL 友好的
      【解决方案8】:

      找出答案的最好方法是让 WebAPI 自己生成预期的 URL 格式:

      public class OffsetController : ApiController
      {
          [Route("offset", Name = "Offset")]
          public IHttpActionResult Get(System.DateTimeOffset date)
          {
              return this.Ok("Received: " + date);
          }
      
          [Route("offset", Name = "Default")]
          public IHttpActionResult Get()
          {
              var routeValues = new { date = System.DateTimeOffset.Now };
              return this.RedirectToRoute("Offset", routeValues);
          }
      }
      

      当调用 /offset 时,响应将返回 302 到包含查询字符串中的“日期”参数的 url。 它看起来像这样:

      http://localhost:54955/offset?date=02/17/2015 09:25:38 +11:00

      我找不到 DateTimeOffset.ToString() 的重载,它会生成该格式的字符串值,除非以字符串格式显式定义格式:

      DateTimeOffset.Now.ToString("dd/MM/yyyy HH:mm:ss zzz")
      

      希望对您有所帮助。

      【讨论】:

        【解决方案9】:

        对于那些正在使用日期时间在客户端和服务器之间寻找某种同步的人来说,这是最简单的方法。我为移动应用程序实现了这一点。它独立于客户的文化。因为我的移动应用程序支持多种文化,并且在这些文化之间使用格式很无聊。感谢.net 有一个完善的函数叫做ToFileTimeFromFileTime

        服务器控制器操作:

        [HttpGet("PullAsync")]
        public async Task<IActionResult> PullSync(long? since = null, int? page = null, int? count = null)
        {
            if (since.HasValue) 
                DateTimeOffset date = DateTimeOffset.FromFileTime(since.Value);    
        }
        

        客户端

        DateTimeOffset dateTime = DateTime.Now.ToFileTime();
        var url= $"/PullAsync?since={datetime}&page={pageno}&count=10";
        

        【讨论】:

        • DateTime.Now.ToFileTime() 返回一个长整数。您不能将长整数分配给 DateTimeOffset - 这不会编译。我认为您的意思是在客户端使用 long 而不是 DateTimeOffset
        • 另外,虽然这会起作用,但它不会传达任何偏移信息。该值本身基于 UTC,然后 DateTimeOffset.FromFileTime 函数将在创建 DateTimeOffset 对象时将其转换为 local 时区。所以如果服务端和客户端不在同一个时区,就会有不同的表示。
        • 此外,Windows 文件时间值基于自 1601-01-01 UTC 以来的 100ns 间隔。这不是我们通常在网络上看到的。更常见的表示是自 1970-01-01 UTC 以来的秒数或毫秒数。因此,如果您有任何非 Windows 客户端,使用 Windows 文件时间的 API 可能会令人困惑(并非不可能,只是不直观)。通常,在创建 Web API 时,应该更喜欢基于 ISO 8601 的基于标准的表示。
        • 当然,如果您不关心这些,只是想要一个快速的内置函数来传递时间戳,那么当然 - 选择您喜欢的任何表示。 ;)
        • @MattJohnson 还有 ToFileTimeUtc 和 FromFileTimeUtc,如果你想要 Utc,你可以使用这两个来代替。我的目的不是获取 UTC 时间,我只想获取我从客户端发送的内容。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-06-12
        • 2016-08-27
        • 1970-01-01
        • 2022-06-16
        • 1970-01-01
        相关资源
        最近更新 更多