Breeze / breeze-client

Breeze for JavaScript clients
MIT License
38 stars 16 forks source link

Saving date & time #61

Open softronsts opened 3 years ago

softronsts commented 3 years ago

Hi, I have gone through various posts on the subject issue. Spent many hours.

quote: "Breeze does not manipulate the datetimes going to and from the server in any way" - Jay Traband unquote

This seems to be quite incorrect. Breeze indeed via AjaxPostWrapper does Json.Stringify() which changes the date from its original form.

All dates going to server are wrong in our production system. We are hunting for solution to quickly resolve this issue.

marcelgood commented 3 years ago

Breeze itself does not manipulate dates, but JSON serialization does. Dates always get stringyfied as UTC in JSON. Breeze can't really control that. JavaScript also automatically converts every date to the local time zone of the browser if the date contains an offset, otherwise it assumes that it already is in the local time zone. It's up to the server to convert the UTC date from JSON to the proper time zone offset. Newtonsoft for example has configuration settings for how you want to treat the dates.

It's also crucial to use a DateTime format in your database that contains time zone information. For example in SQL Server you should use DateTimeOffset and not DateTime. The latter has no time zone information, so your server logic has to know what time zone they are in and convert them if necessary. FYI, if you use EF with DateTime, then DateTime.Kind is always undefined, and in most cases undefined is treated the same as local. You can use a value converter to set the DateTime.Kind correctly. For example you can set it to UTC if all your dates in your DB are stored in UTC, or you can set it to Local if you store them in the server local time zone.

Dates are a real pain in the web world with JavaScript, and you have to be very careful to make sure you store them with the correct time zone information in the DB, and convert them if necessary to display correctly in the browsers.

steveschmitt commented 3 years ago

There is another way in which Breeze "manipulates" the dates - if the date comes from the server (as a string, in JSON) without a time zone specifier, Breeze changes the incoming string to add a "Z" (UTC) specifier to the end before parsing.

So Breeze will cause a date without time zone to be interpreted as UTC, when it otherwise might be interpreted as either local time or UTC, depending upon the browser implementation. (This decision was made years ago, when browser date parsing was highly variable; things are probably more consistent now.)

The code for this is buried the in Breeze DataTypes code, so if you don't like this behavior, you can do this:

import { breeze } from 'breeze-client';
...
// change Breeze date parsing to not infer UTC timezone
breeze.DataType.parseDateFromServer = (source: any) => new Date(Date.parse(source));

This change makes it so that, when Dates are received without timezone, Breeze will parse them directly (into the browser's local time, usually).

This has its own set of problems, of course, when the server and browser are in different time zones, or when the data was saved from a different time zone.

softronsts commented 3 years ago

There is no issue in processing the date coming in from the server to client i.e parseDateFromServer already in our implementation.

Issue only in sending the date from client to server.

After giving up fixing this issue outside breeze we had to tweak in your source. We have made following the changes and tested and working good so far. We will do further testing and advise if any issues.

in AbstractDataServiceAdapter under saveChanges()


 // let bundle = JSON.stringify(saveBundleSer);  -- original implementation

    let bundle = JSON.stringify(saveBundleSer, (key, value) => {

      return (this._jsonReplacer(key, value));

    });


_jsonReplacer(key: any, value: any) {

    return key === "" ? this.convertIfDate(value) : value;

  }

  convertIfDate(value: any) {
    if (value.entities === undefined) {
      return value;
    }
    value.entities.forEach((v: any) => {
      v = this.recurse(v)
    })

    return value;
  }

  recurse(v: any) {
    for (const [key, value] of Object.entries(v)) {

      if (value !== null && value instanceof Object && Object.prototype.toString.call(value) !== "[object Date]") {
        this.recurse(value)
      } else if (value !== null && Object.prototype.toString.call(value) === "[object Date]") {
        var date = new Date(Date.parse(value + ""))
        v[key] = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString();
      }
    }

    return v;
  }

Would you be kind enough to review the above code and advise if I can make pull request or you can fine tune the above code and provide us the changes from your end?

marcelgood commented 3 years ago

No, this is not something that should be done in the Breeze source code. You are effectively creating a fake UTC date for the browser's local time zone. What's your server written in? You should configure the time zone handling on the server. Following example is for .NET Core with Newtonsoft. In this case Newtonsoft converts the UTC date time back to local time. There are other options depending on what you need.


            {
                // Set Breeze defaults for entity serialization
                var ss = JsonSerializationFns.UpdateWithDefaults(opt.SerializerSettings);
                if (ss.ContractResolver is DefaultContractResolver resolver)
                {
                    resolver.NamingStrategy = null;  // remove json camelCasing; names are converted on the client.
                }
                ss.Formatting = Newtonsoft.Json.Formatting.Indented; // format JSON for debugging
                ss.DateTimeZoneHandling = DateTimeZoneHandling.Local;
            });```
steveschmitt commented 3 years ago

@softronsts Can you please provide some specific examples of the problem?

softronsts commented 3 years ago

Our tech stack: Client - Angular 9 Server - .NET Core 3.1 DB - MS SQL Server

Our use case is on vessel scheduling ( similar to flight scheduling ) info shared with users in other time zones.

PORT ----- ARRIVAL ----- DEPARTURE

THAILAND - 26/10/2021 09:00 ----- 28/10/2021 15:00 SINGAPORE - 11/11/2021 23:00 ----- 12/11/2021 08:00 LOS ANGELES - 25/11/2021 12:00 ----- 27/11/2021 05:00

Server time zone is SINGAPORE in this case.

When the user in THAILAND updates the arrival & departure date we do not want to convert to local time zone of SINGAPORE. Date & time of THAILAND should be intact. Hence we cannot use DateTimeZoneHandling = DateTimeZoneHandling.Local

Note: our current setting is DateTimeZoneHandling = DateTimeZoneHandling.Utc

We do not want to store as DateTimeOffset as everyone should see/read the date & time as it is entered by each location without conversion to each user's local time. If we store the date & time with time zone then displaying the exact date & time for the each user time zone requires specific conversions. Visually everyone reads the date/time along with the location name on the side.

What is happening with current breeze functionality is 1) Assuming user in UTC +8 entering the date & time as 25/10/2021 09:00 - breeze sends as 25/10/2021 01:00 - without time zone info there is no possible way to interpret at the server the correct time sent by the client. 2) if we change the database field to DateTimeOffset then the date is stored as 25/10/2021 09:00 +8.00 but when user in other location/times fetches this date it is converted to their local time with difference i.e LOS ANGLESES should read THAILAND time as 25/10/2021 09:00 instead it will be shown as 24/10/2021 18:00 (PM) 3) if we use breeze.DataType.parseDateFromServer with the field defined as DateTimeOffset

breeze.DataType.parseDateFromServer = (source: any) => new Date(Date.parse(source));
        or
var date = moment(source).toDate();

effect is same as mentioned in the point no.2 and we loose the original time zone when sent back to the server i.e when LA user saves the schedule then it goes to the server with THAILAND Arrival date as 24/10/2021 18:00 as opposed to 25/10/2021 09:00 which is an erroneous info.

Hence we had to conclude using only 'DateTime' field in the backend and store the date & time without time zone and send the date & time as entered by the user. It is still UTC as per each user's time zone.

Please let me know if you need any further clarifications on the above. Looking forward to alternative solution if the same can be achieved outside BreezeJS.

softronsts commented 3 years ago

I see similar case is already handled in DataService OData Adapter for the DateTimeOffset data type

image

marcelgood commented 3 years ago

@softronsts yes, this is a common issue. Effectively you have to send the browser's time zone offset, or location along, so the server can convert the UTC datetime to the local time zone of the browser's location and store it in the database as such, and not the servers time zone. But be aware that if a user in say New York looks at the date in their browser it may not display correctly, because the browser always converts dates to the local time zone of the browser if it comes down with an offset. If it comes down from the server w/o an offset, then it's treated as local time, but then you have an issue if the user in New York saves the date back to the DB. In situations where a date is just a date, it's often easier to store it as a string in the DB. .NET 6 will have a new data type called DateOnly to deal with these situations better, but have to wait for SQL Server or whatever DB you are using to support it as well and we'll have to support it in Breeze.

softronsts commented 3 years ago

Date only is not an issue here. Date + time is the issue.

I guess .NET 6 DateOnly & TimeOnly not going to solve this problem either.

I strongly believe this issue needs to be handled within Breeze. Hope you can consider.

steveschmitt commented 3 years ago

I don't think your change is a bad idea. It's a way for the client to communicate the time offset to the server, instead of making the server guess.

But we would have to make it a configuration option to turn that on - it would break a lot of apps if it became the breeze default behavior.

softronsts commented 3 years ago

Configuration option would be a best way to go to solve this issue which many are facing.

You are right about it would break a lot of apps - including our current project. One such issue is that UI would show the converted date inside the entitymanager and should be reloaded immediately after save response.