Asp.Net、SQL和时区



有人问过这个问题,但我正在努力掌握如何在web应用程序中处理时区的概念。我有一个跟踪项目进度的系统。我有一个ProjectStartDate日期在我的SQL Server数据库。(还有更多的字段和表,但我们只关注一个)。

服务器位于美国的某个地方。我住在澳大利亚。

调用SELECT GETDATE()返回"2013-08-11 14:40:50.630"我的系统时钟显示"2013-08-12 07:40">

在我的数据库中,我在所有表上都有'CreateDateTime'列。当我在c#代码中存储时,我使用CreateDate = DateTime.UtcNow

我使用它,因为我听说使用UTC更好。

但是,当用户看到一个日历控件,并且他们为一个项目选择一个开始日期时,我将存储用户所选择的内容。没有转换…我说过,StartDate是数据库中的DATE类型。

问题是,如果一个项目今天开始-我的前端说当前项目是没有开始的,因为服务器还在昨天。

我认为日期应该像我存储它们一样存储。但也许我需要获取用户时区,并应用到UI层面?

我看到的问题是:

  • 我不知道用户的时区。添加一些允许他们选择的东西?
  • 项目的状态可能在存储过程中确定,因此何时可以应用转换?在这个过程中,它可能会做一个检查,如果StartDate <= DateTime.Now?

我使用EntityFramework和Linq来获取数据的大部分时间。我需要一种插入和检索数据的策略,既要从SQL的角度,也要从。net的角度。

我已经添加了代码,让用户选择他们的时区为基础:

public List<TimeZoneDto> GetTimeZones()
{
var zones = TimeZoneInfo.GetSystemTimeZones();
var result = zones.Select(tz => new TimeZoneDto
{
Id = tz.Id, 
Description = tz.DisplayName
}).ToList();
return result;
}

然后保存在他们的配置文件中。

所有日期都以UTC格式存储,如下面的答案所示。

我仍然很困惑如何处理从数据库到客户端的日期。下面是我如何存储记录的一个例子:

public int SaveNonAvailibility(PersonNonAvailibilityDto n)
{
person_non_availibility o;
if (n.Id == 0)
{
o = new person_non_availibility
{
PersonId = n.PersonId,
Reason = n.Reason,
StartDate = n.StartDate,
EndDate = n.EndDate,
NonAvailibilityTypeId = n.NonAvailibilityTypeId,
CreateUser = _userId,
CreateDate = DateTime.UtcNow
};
_context.person_non_availibility.Add(o);
}
else
{
o = (from c in _context.person_non_availibility where c.Id == n.Id select c).FirstOrDefault();
o.StartDate = n.StartDate;
o.EndDate = n.EndDate;
o.Reason = n.Reason;
o.NonAvailibilityTypeId = n.NonAvailibilityTypeId;
o.LastUpdateDate = DateTime.UtcNow;
o.LastUpdateUser = _userId;
o.Deleted = n.Deleted ? DateTime.UtcNow : (DateTime?)null;
}
_context.SaveChanges();
return o.Id;
}

这个方法基本上可以在一个人不能工作的时候节省时间。注意我存储LastUpdateDate的方式。此外,"开始"one_answers"结束"日期。这些日期更多的是"商务"日期。

选择,然后日期检查,是我有问题的地方。在这个例子中,我得到了一个基于NOW的个人费率。

public decimal CurrentRate
{
get
{
if (ResourceCosts != null)
{
var i = ResourceCosts.FirstOrDefault(t => DateTime.UtcNow <= (t.EndDate.HasValue ? t.EndDate.Value : DateTime.UtcNow) && DateTime.UtcNow >= t.StartDate);
if (i != null) return i.Cost;
return 0;
}
return 0;
}
}

所以,我在这里要做的是,基于当前日期,我想看到他的费率(因为他收取我们的费率从1月1日到1月15日可能是100美元,然后从1月16日到1月31日是110美元),所以我寻找今天适用的费率(如果有的话)。这不能跨时区工作,它可能在这里,我需要做一些日期操作基于'DateTime.UTCNow'?

注意,我现在根据上面存储用户时区的代码知道了用户的时区。我可以在这里用它吗?也许,当用户登录时,从他的配置文件(Zimezone信息)中获取日期信息,然后有一个全局共享函数,返回用户datetime,基于从UTC日期添加或删除小时…并在我做datetime。utcnow时使用它?

希望有人能指点一下。

您可能会发现没有一种"正确"的方法来处理所有这些。你在问题中描述的几个不同问题有多种解决方法。我将试图澄清几点。

  • 首先,不要试图考虑服务器上的本地时间。您的代码和数据不应该根据部署位置而改变。您说您的服务器在美国,但是要考虑多个时区,并且许多服务器将其时区设置为UTC,而不管其物理位置如何。

    您应该避免在SQL Server中使用GETDATE()SYSDATETIME()。如果您需要SQL中的当前时间戳,请使用GETUTCDATE()SYSUTCDATETIME()。如果由于某种原因服务器的时区对您很重要,请使用SYSDATETIMEOFFSET()

    同样地,避免在. net中的任何服务器端代码中使用DateTime.Now。使用DateTime.UtcNowDateTimeOffset.UtcNow作为UTC时间戳,或者如果由于某些原因服务器的时区对你很重要,使用DateTimeOffset.Now。 你可以在我的博客文章《反对日期时间的案例》中了解更多。现在
  • 接下来,让我们讨论一下您正在使用的数据类型。SQL Server中的date类型只存储一个日期。就是这样。没有时间,没有偏移,没有时区。2013-08-11就是一个例子。当你真正指的是整个日历日期时,应该使用它。"今天"在世界范围内没有统一的背景。相反,每个人都根据自己的时区有自己的意思。此外,并非每个日历日都是24小时。一天可能是23、23.5、24、24.5或25小时,这取决于夏令时在特定时区的应用方式,以及您是否正在评估夏令时转换的日期。

    在。net中没有Date类型,所以SQL的date被转换为DateTime,时间设置为午夜(00:00:00),类型设置为Unspecified。但是不要欺骗自己——时间并不是突然变成了午夜,我们只是在为时间填上0。这可能会导致很多错误和混乱。(如果你想避免这种情况,你可以尝试Noda Time,它有一个LocalDate类型用于此目的。)

  • 你真正需要考虑的,但在你的问题中没有定义的是:

    什么确切的时刻开始一个项目?

    现在你只是在说2013-08-11,这并不是指一个特定的时刻。您是指某特定时区当天的开始吗?或者您指的是根据用户的时区的那天的开始?这可能不是一回事。你不能拿别人的"现在"(utc、本地或其他)来比较,除非你知道你在谈论的是什么时刻。

    如果项目在世界范围内的一个精确时刻开始,那么最简单的事情就是存储包含UTC精确时间的datetime(或datetime2)类型。所以你可能会说一个项目开始于2013-08-10T14:00:00Z,也就是8月11日在澳大利亚悉尼的午夜。在。net中,您将使用DateTime类型,并将.Kind设置为Utc

    另一种表示方式是存储一个datetimeoffset类型,它的值为2013-08-11T00:00:00+10:00——它在时间上是相同的,但是使用偏移量给你一个预转换的值。(悉尼在那天是UTC+10)。您可以在。net中使用DateTimeOffset类型来处理此操作。

    但是如果项目根据用户的不同在不同的时间开始,那么它就不是一个确切的时间点。这更像是一个"浮动"的开始。如果来自世界各地的用户被分配到同一个项目,那么一些用户可能会在其他用户之前开始。如果这是您的意图,那么如果所有项目都在午夜开始,那么您可以使用date类型,或者如果项目可能在不同时间开始,您可以使用datetime或(datetime2)类型。在。net代码中,您将使用DateTime类型,并将.Kind设置为Unspecified

  • 关于获取用户的时区,你能做的最好的事情就是询问他们。尽管存在普遍的误解——你不能仅仅从浏览器中获取它。你能从浏览器得知的只是它们的当前偏移量是多少。(请阅读时区标签wiki的"TimeZone != Offset"部分)。

    当询问用户的时区时,如果您决定使用Windows时区,您可以从TimeZoneInfo.GetSystemTimeZones方法生成一个下拉列表。.Id是存储在数据库中的键,并向用户显示.DisplayName。稍后,您可以使用TimeZoneInfo.FindSystemTimeZoneById方法来获得一个TimeZoneInfo对象,您可以将其用于转换。

    如果您想要更精确,您可以使用IANA时区而不是Windows时区。为此,我建议使用基于地图的时区选择器控件,比如这个。您还可以使用jsTimeZoneDetect来猜测控件的默认值。在服务器上,您将使用Noda Time执行时区转换。

  • 有一种方法不需要时区转换。基本上,您在UTC中执行所有操作。这包括将时间以UTC格式传输到浏览器。然后你可以使用JavaScript获取用户的当前时间在UTC,并与之比较。

    如果您愿意,可以使用JavaScriptDate类的各种函数来完成此操作。但是你可能会发现使用像moment.js这样的库更容易。

    虽然这种方法对许多事情都是可行的,但安全性不是其中之一。你的用户可以很容易地改变他们的计算机时钟来解决这个问题。

  • 另一种方法是比较服务器端与UTC。如果你的数据库中有确切的UTC开始时间,那么你可以在。net代码中检查DateTime.UtcNow,并使用它来决定要做什么。您不需要用户的时区来进行这种比较,但如果您想向他们显示其在当地时间中的含义,则需要它。

我希望这澄清了混乱,没有使情况变得更糟!:)如果您有其他问题,请编辑您的问题或在评论中提出。

关于你最新的问题,我建议你试试以下方法:

var timeZoneId = "Eastern Standard Time"; // from your user's selection
var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
var nowInTimeZone = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timeZone);
var todayInTimeZone = nowInTimeZone.Date;
var i = ResourceCosts.FirstOrDefault(t => t.StartDate <= todayInTimeZone &&
(!t.EndDate.HasValue || t.EndDate >= todayInTimeZone));

当然,这意味着在您的StartDateEndDate字段中,您不会将这些存储为UTC -而是作为与用户相关的"业务日期"。当你应用一个时区时,这些时间只对应于一个特定的时刻,所以相同的UTC时间戳可能落在不同的日期,这取决于用户所在的时区。

此外,您正在使用完全包含的范围,这对于这类日历日期范围通常是可以的。但要确保你意识到可能会有重叠。因此,如果您有2013-01-01 - 2013-02-012013-02-01 - 2013-03-01,那么有一天2013-02-01都在范围内。

解决这个问题的一个常见方法是使用半开间隔[start,end)。换句话说,start <= now && end > now。但是,当使用完整的日期和时间而不是仅仅使用日期时,这种情况更常见。您可能不需要这样做,但您至少应该为您的特定场景考虑一下。

您可以从浏览器获取用户的时区。这将只获得他们在计算机上设置的值。所以他们可以利用这一点。例如,如果您将访问限制在当地时间午夜之前,他们可以更改时间以获得"早期"访问。所以请记住这一点。

接下来你要做的是在数据库中存储UTC格式的所有时间。你可以用GetUTCDate()代替GetDate()。

接下来,您可以存储它们的时区,或者它们在数据库中与UTC的偏移量。仅仅存储UTC偏移量会有一些危险,因为遵守夏令时的时区在一年中的不同时间会有不同的偏移量。

但是有了这两条信息,你就可以计算出用户的"本地"时间。您可以将小时数添加到存储的日期,然后将其与本地服务器时间(也从UTC调整)进行比较。

如果你真的不关心那么多,你可以在数据库中存储UTC时间,然后简单地比较DateTime。

相关内容

  • 没有找到相关文章

最新更新