14

I've got a WebAPI action that looks like so:

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

But when I invoke this from a HttpClient instance, making a URL like:

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"

I get a 400 response back saying that the non-nullable parameter date does not exist.

I've also tried adding the [FromUri] attribute to the parameter but it still doesn't map through. If I change it to be DateTimeOffset? I can see it is left as null and looking at Request.RequestUri.Query the value is there, just not mapped.

Finally I tried not doing a WebUtility.UrlEncode and it made no different.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
Aaron Powell
  • 24,927
  • 18
  • 98
  • 150

10 Answers10

20

Answer

To send a DateTimeOffset to your API, format it like this after converting it to UTC:

2017-04-17T05:04:18.070Z

The complete API URL will look like this:

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

It’s important to first convert the DateTimeOffset to UTC, because, as @OffHeGoes points out in the comments, the Z at the end of the string indicates Zulu Time (more commonly known as UTC).

Code

You can use .ToUniversalTime().ToString(yyyy-MM-ddTHH:mm:ss.fffZ) to parse the DateTimeOffset.

To ensure your DateTimeOffset is formatted using the correct timezone always use .ToUniversalTime() to first convert the DateTimeOffset value to UTC, because the Z at the end of the string indicates UTC, aka "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);
Community
  • 1
  • 1
Brandon Minnick
  • 13,342
  • 15
  • 65
  • 123
  • ":" is an invalid character in a web uri see: https://stackoverflow.com/questions/2366260/whats-valid-and-whats-not-in-a-uri-query – rollsch Jul 25 '17 at 01:20
  • 2
    Note that formatting as `yyyy-MM-ddTHH:mm:ss.fffZ` is forcing the date to be formatted as a string **without timezone offset**...which is fine in this example because the value is `DateTimeOffset.UtcNow` but if you were to use a non-UTC value then this format will not represent the actual date. – oatsoda May 15 '19 at 08:06
  • Good catch, @OffHeGoes! I updated the answer to credit your comment – Brandon Minnick May 15 '19 at 16:09
  • What's sad is that despite getting lots of upvotes (and multiple edits), this answer is still TOTALLY AND COMPLETELY WRONG. It throws away the offset, which, as one might get by the name, IS THE ENTIRE POINT of DateTimeOffset. And to be super clear, if you use this code, once the DateTimeOffset has been deserialized, you will have the correct time, but will have no clue of the time difference between UTC and the time zone which an event occurred. – Jack Bond Oct 28 '21 at 15:20
20

The current accepted answer throws away the time zone information, which in some cases is important. The following maintains the time zone and doesn't lose any precision. It also keeps your code succinct when building a query string.

public static string UrlEncode(this DateTimeOffset dateTimeOffset)
{
     return HttpUtility.UrlEncode(dateTimeOffset.ToString("o"));
}
Jack Bond
  • 448
  • 3
  • 8
  • 3
    This is *absolutely* the correct answer. The other answers don't address a) The idea that offset should either be "Z" (for UTC) or "+xx:xx" (for non-UTC), and b) The problem that the +xx:xx offset is not url friendly due to the "+" – oatsoda May 15 '19 at 08:16
  • Intellisense says o is not valid, but O is, and that works for me. (Not sure what's going on there.) – user169771 Sep 23 '22 at 14:21
  • This is the correct answer and solves my problem with dateTimeOffset as parameter – Zokka Mar 31 '23 at 08:50
5

The problem is the + (plus) char in the offset part, we should encode that.

If the offset is - (minus) no need to encode

Encoded value of +(plus) is %2B So the 2021-05-05T18:00:00+05:00 should be passed as 2021-05-05T18:00:00%2B05:00

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

if the offset is - (minus) then

http://localhost:1234/api/values/1?date=2021-05-05T18:00:00-05:00
Sukesh Chand
  • 2,339
  • 2
  • 21
  • 29
2

The problem is being described exactly by the 400 response message, although it could have been more clear. The route, as defined by the attribute, only expects a parameter id, but the Delete method expects another parameter called date.

If you want to provide this value using the query string, you'll need to make that parameter nullable, by using "DateTimeOffset?", which would also transform it into an optional parameter. If the date is a required field, consider adding it to the route, like:

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

OK, ignore what I typed above, it's just a formatting problem. Web API has trouble figuring out the culture needed to parse the given value, but if you try to pass on DateTimeOffset using a JSON format in the query string, like 2014-05-06T22:24:55Z, that should work.

Brandon Minnick
  • 13,342
  • 15
  • 65
  • 123
Wesley Cabus
  • 121
  • 1
  • 1
  • 7
1

To achieve this, I'm using

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);
    }
}
Dejan
  • 9,150
  • 8
  • 69
  • 117
  • : is not a valid character by default. – rollsch Jul 25 '17 at 01:11
  • ":" is an invalid character in a web uri see: https://stackoverflow.com/questions/2366260/whats-valid-and-whats-not-in-a-uri-query – rollsch Jul 25 '17 at 01:20
  • So this implies that you can only ever pass the date value to the API as a UTC value? Seems a bit limited...you'd expect to be able to pass any timezone - isn't that the one of the benefits of ISO 8601? – oatsoda May 15 '19 at 08:04
1

Create a custom type converter as follows:

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

In your startup sequence, such as WebApiConfig.Register, add this type converter dynamically to the DateTimeOffset struct:

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

You can now just pass DateTimeOffset values in the compact form of ISO8601, which omits hyphens and colons that interfere with the URL:

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

Note that if you have fractional seconds, you may need to include a trailing slash in the url.

api/values/20171231T012345.1234567-0530/

You could also put it in the querystring if you like:

api/values?foo=20171231T012345-0530
Community
  • 1
  • 1
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • I was able to get this approach to work by copying the source of the framework `DateTimeOffsetConverter`, and then adding the line `input = Uri.UnescapeDataString(str);` – ZacharyB Jun 09 '17 at 14:50
  • 1
    you dont need to have such converter. there is already function on both client and server side for you. see my answer please https://stackoverflow.com/a/51011977/492482 – Emil Jun 24 '18 at 16:50
1

Use the ISO 8601 datetime format specifier:

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

or

$"http://localhost:1234/api/values/1?date={DateTime.UtcNow:o}"
Andriy Tolstoy
  • 5,690
  • 2
  • 31
  • 30
0

You can use:

$"date={HttpUtility.UrlEncode($"{DateTimeOffset.Now:o}")}"
Snaketec
  • 471
  • 2
  • 14
-1

Best way to find out is to ask the WebAPI to generate the expected URL format itself:

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

When a call to /offset is made the response will return a 302 to a url that contains the 'date' parameter in the querystring. It will look something like this:

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

I could not find an overload of DateTimeOffset.ToString() that would generate a string value in that format except for explicitly defining the format in a string format:

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

Hope that helps.

Gavin Osborn
  • 2,593
  • 3
  • 27
  • 42
-1

Here is the easiest way for those who are looking for some kind of sync between client and server using datetime. I implemented that for mobile application. It is independent from the culture of the client. because my mobile app supports multiple cultures and it is boring to use formatting between those cultures. thanks that .net has a perfect functions called ToFileTime and FromFileTime

Server Controller Action:

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

Client Side

DateTimeOffset dateTime = DateTime.Now.ToFileTime();
var url= $"/PullAsync?since={datetime}&page={pageno}&count=10";
Brandon Minnick
  • 13,342
  • 15
  • 65
  • 123
Emil
  • 6,411
  • 7
  • 62
  • 112
  • `DateTime.Now.ToFileTime()` returns a long integer. You can't assign a long integer to a `DateTimeOffset` - that won't compile. I think you meant to have a `long` instead of a `DateTimeOffset` on the client side. – Matt Johnson-Pint Jun 24 '18 at 19:09
  • Also, while this will work, it doesn't convey any offset information. The value itself is based on UTC, and then the `DateTimeOffset.FromFileTime` function will convert it to the *local* time zone as it creates the `DateTimeOffset` object. So if the server and client are not in the same time zone, you will have different representations. – Matt Johnson-Pint Jun 24 '18 at 19:12
  • Additionally, a Windows File Time value is based on 100ns intervals since 1601-01-01 UTC. That's not something we typically see on the web. A more common representation is in terms of seconds or milliseconds since 1970-01-01 UTC. Thus, if you have any non-Windows clients, an API that uses Windows File Times may be confusing (not impossible, just not intuitive). Generally, one should prefer standards-based representations based on ISO 8601 when creating Web APIs. – Matt Johnson-Pint Jun 24 '18 at 19:16
  • Of course, if you don't care about any of that and just want a quick built-in function to pass timestamps, then sure - choose any representation you like. ;) – Matt Johnson-Pint Jun 24 '18 at 19:17
  • @MattJohnson there is also ToFileTimeUtc and FromFileTimeUtc, you can use these 2 instead if you want Utc. my purpose wasnt get the utc time, i only wanted to get what i send from client. – Emil Jun 25 '18 at 09:22