将 DateTimeOffset 作为 WebAPI 查询字符串传递

Passing DateTimeOffset as WebAPI query string

我有一个如下所示的 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 回复说不可为 null 的参数 date 不存在。

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

最后我试着不做 WebUtility.UrlEncode 也没有什么不同。

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

的参数

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

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

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

找出答案的最佳方法是让 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 时,响应将 return 一个 302 到 url,它在查询字符串中包含 'date' 参数。 它看起来像这样:

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")

希望对您有所帮助。

为此,我使用了

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);
    }
}

回答

要将 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 points out , the Z at the end of the string indicates 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);

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

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

使用 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}"

对于那些正在使用日期时间在客户端和服务器之间寻找某种同步的人来说,这是最简单的方法。我为移动应用程序实现了它。它独立于客户的文化。因为我的移动应用程序支持多种文化,在这些文化之间使用格式很无聊。感谢 .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";

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

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

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

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

+(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