Передача DateTimeOffset в виде строки запроса WebAPI

У меня есть действие 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, говорящий, что параметр, не имеющий значения nullable date не существует.

Я также попытался добавить атрибут [FromUri] к параметру, но он по-прежнему не отображается. Если я изменю его на DateTimeOffset?, я вижу, что он оставлен как null и смотрит на Request.RequestUri.Query, это значение имеет значение, просто не отображаемое.

Наконец, я попытался не делать WebUtility.UrlEncode, и он не изменился.

Ответ 1

Проблема описывается точно в ответном сообщении 400, хотя оно могло бы быть более ясным. Маршрут, как определено атрибутом, ожидает только идентификатор параметра, но метод Delete ожидает другой параметр с именем date.

Если вы хотите предоставить это значение, используя строку запроса, вам нужно сделать этот параметр обнуляемым, используя "DateTimeOffset?", Который также преобразует его в необязательный параметр. Если дата является обязательным полем, рассмотрите возможность добавления ее в маршрут, например:

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

ОК, игнорируйте то, что я напечатал выше, это просто проблема форматирования. В Web API возникают проблемы с выяснением культуры, необходимой для анализа заданного значения, но если вы попытаетесь передать DateTimeOffset с использованием формата JSON в строке запроса, например 2014-05-06T22: 24: 55Z, это должно сработать.

Ответ 2

Ответ

Чтобы отправить DateTimeOffset вашему API, отформатируйте его следующим образом после преобразования в UTC:

2017-04-17T05:04:18.070Z

Полный URL API будет выглядеть так:

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

Важно сначала преобразовать DateTimeOffset в UTC, потому что, как указывает @OffHeGoes в комментариях, Z в конце строки указывает время Зулу (более широко известное как UTC).

Код

Вы можете использовать .ToUniversalTime().ToString(yyyy-MM-ddTHH:mm:ss.fffZ) для анализа DateTimeOffset.

Чтобы убедиться, что ваш DateTimeOffset отформатирован с использованием правильного часового пояса, всегда используйте .ToUniversalTime() чтобы сначала преобразовать значение DateTimeOffset в UTC, потому что Z в конце строки указывает UTC, то есть "Zulu Time".

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

Ответ 3

Создайте конвертер настраиваемого типа следующим образом:

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

Теперь вы можете просто передать значения DateTimeOffset в компактной форме ISO8601, которая не допускает дефисов и двоеточий, которые мешают URL:

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

Обратите внимание, что если у вас есть дробные секунды, вам может потребоваться включить трейлинг-косу в URL.

api/values/20171231T012345.1234567-0530/

Вы также можете поместить его в очередь, если хотите:

api/values?foo=20171231T012345-0530

Ответ 4

Чтобы достичь этого, я использую

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

Ответ 5

Используйте спецификатор 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}"

Ответ 6

Текущий принятый ответ отбрасывает информацию о часовом поясе, что в некоторых случаях важно. Следующее поддерживает часовой пояс и не теряет никакой точности. Он также сохраняет ваш код лаконичным при построении строки запроса.

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

Ответ 7

Лучший способ узнать, должен ли 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);
    }
}

Когда выполняется вызов/смещение, ответ возвращает 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")

Надеюсь, что это поможет.

Ответ 8

Вот самый простой способ для тех, кто ищет какую-то синхронизацию между клиентом и сервером, используя datetime. Я реализовал это для мобильного приложения. Это не зависит от культуры клиента. потому что мое мобильное приложение поддерживает несколько культур, и скучно использовать форматирование между этими культурами. спасибо, что .net имеет совершенные функции, называемые ToFileTime и FromFileTime

Действие контроллера сервера:

[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";