如何正确地将 JS 日期发送到 EF Core5 并返回?
How to properly send JS dates to EF Core5 and back?
更新:我已经在此处添加了修复程序,以供任何正在寻找其正常工作的前后示例的人使用。
我知道这有点重复,但我似乎一直在寻找部分答案,但没有找到完整的解决方案。我看到很多关于在后端以 UTC 格式存储日期的评论,我确实想这样做,但是 how/where 上没有真正的整体解决方案来处理过去几年内的翻译。
我知道 JS Date()
对象存储时区信息,然后当您在其上使用 JSON.stringify()
时,它会将其交换为 UTC 时间。这将时间提前 7 小时(对于我的语言环境),因此每次我保存日期时它都会提前 7 小时。我该如何解决?我确实希望将日期作为 UTC 存储在数据库中,但我不确定我需要在哪里执行转换才能将它从 UTC 返回到我的语言环境时间,当它 returns 到浏览器时。
我试图在下面包含整个代码路径,希望有人能指出我遗漏的内容。
首先,我有两个 EFCore5 类,我用它们以数据优先的方法构建我的数据库。一个income Source和一个Schedule,这些对象之间是一对一的关系。
public class Schedule
{
[Key]
public int ScheduleID { get; set; }
[Required]
public ScheduleFrequency Frequency { get; set; }
[DataType(DataType.Date)]
public DateTime Occurrence_First { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_LastConfirmed { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_LastPlanned { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_Final { get; set; }
public bool IsAutoConfirm { get; set; }
[DataType(DataType.Date)]
public DateTime DateTime_Created { get; set; }
[DataType(DataType.Date)]
public DateTime? DateTime_Deactivated { get; set; }
public bool HasCustomTransactionTime { get; set; }
[System.Text.Json.Serialization.JsonIgnore]
public IncomeSource IncomeSource { get; set; }
}
public class IncomeSource
{
[Key]
public int IncomeSourceID { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal ExpectedAmount { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal TotalFromSource { get; set; }
public int? DefaultToAccountID { get; set; }
public int ScheduleID { get; set; }
public BudgetorAccount Account { get; set; }
public Schedule Schedule { get; set; }
public int AccountID { get; set; }
[ForeignKey("DefaultToAccountID")]
public BudgetorAccount DefaultToAccount { get; set; }
}
public class BudgetorDbContext : DbContext
{
public BudgetorDbContext(DbContextOptions<BudgetorDbContext> options) : base (options)
{
}
public DbSet<BudgetorAccount> Accounts { get; set; }
public DbSet<IncomeSource> IncomeSources { get; set; }
public DbSet<Schedule> Schedules { get; set; }
}
API 控制器调用一个服务来转换to/from 视图模型并处理所有 CRUD 操作。该服务利用 EFCore 自动保存相关对象的能力来保存附加到 IncomeSource
的 Schedule
public class IncomeSourceDetailVM : AccountDetailVM
{
public int IncomeSourceId { get; set; }
public decimal ExpectedAmount { get; set; }
public decimal TotalFromSource { get; set; }
public int? DefaultToAccountID { get; set; }
public Schedule Schedule { get; set; }
public IncomeSourceDetailVM() : base(AccountType.IncomeSource)
{
this.Schedule = new Schedule()
{
Occurrence_First = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day)
};
}
}
public class BudgetorDataService : IBudgetorService
{
public async Task<IncomeSourceDetailVM> GetIncomeSourceDetailVM(int id)
{
var vm = await getIncomeSourceData()
.Where(inc => inc.AccountID == id)
.Select(incSrc => new IncomeSourceDetailVM()
{
IncomeSourceId = incSrc.IncomeSourceID,
ExpectedAmount = incSrc.ExpectedAmount,
AccountId = incSrc.AccountID,
AccountName = incSrc.Account.Name,
DateTime_Created = incSrc.Account.DateTime_Created,
DateTime_Deactivated = incSrc.Account.DateTime_Deactivated,
DefaultToAccountID = incSrc.DefaultToAccountID,
Schedule = incSrc.Schedule,
Notes = incSrc.Account.Notes,
})
.FirstOrDefaultAsync();
return vm;
}
public async Task<IncomeSourceDetailVM> CreateIncomeSource(IncomeSourceDetailVM incomeSourceToAdd)
{
if (incomeSourceToAdd == null)
{
throw new ArgumentNullException(nameof(incomeSourceToAdd));
}
try
{
BudgetorAccount newAcct = new BudgetorAccount()
{
AccountType = AccountType.IncomeSource,
DateTime_Created = DateTime.Now,
Name = incomeSourceToAdd.AccountName,
Notes = incomeSourceToAdd.Notes,
IsSystem = false
};
await _context.Accounts.AddAsync(newAcct);
await _context.SaveChangesAsync();
incomeSourceToAdd.AccountId = newAcct.AccountID;
incomeSourceToAdd.DateTime_Created = newAcct.DateTime_Created;
IncomeSource newIncSrc = new IncomeSource()
{
AccountID = newAcct.AccountID,
DefaultToAccountID = incomeSourceToAdd.DefaultToAccountID,
ExpectedAmount = incomeSourceToAdd.ExpectedAmount,
Schedule = incomeSourceToAdd.Schedule
};
await _context.IncomeSources.AddAsync(newIncSrc);
await _context.SaveChangesAsync();
incomeSourceToAdd.IncomeSourceId = newIncSrc.IncomeSourceID;
}
catch(Exception err)
{
throw err;
}
return incomeSourceToAdd;
}
}
[ApiController]
public class IncSrcController : ControllerBase
{
private readonly IBudgetorService _budgetorService;
public IncSrcController(IBudgetorService budgetorService)
{
this._budgetorService = budgetorService;
}
[HttpPost]
[Consumes("application/json")]
public async Task<ActionResult<IncomeSourceDetailVM>> PostIncSrc([FromBody] IncomeSourceDetailVM incSrcToAdd)
{
IncomeSourceDetailVM createdIncSrc = incSrcToAdd.AccountId == 0
? await this._budgetorService.CreateIncomeSource(incSrcToAdd)
: await this._budgetorService.UpdateIncomeSource(incSrcToAdd);
return StatusCode(201, createdIncSrc);
}
[HttpGet("{id}")]
public async Task<ActionResult<IncSrcManagementVM>> GetAccount(int? id)
{
IncSrcManagementVM result = new IncSrcManagementVM()
{
Account = (id == 0 || !id.HasValue)
? new IncomeSourceDetailVM()
: await this._budgetorService.GetIncomeSourceDetailVM(id.Value),
ToAccounts = await this._budgetorService.GetIncSrcToAccounts()
};
if (result.Account.DefaultToAccountID.HasValue)
{
result.ToAccounts
.Find(s => s.AccountId == result.Account.DefaultToAccountID)
.IsDefault = true;
}
return result;
}
}
在前端,我在形状与上述 C# 中的 IncomeSourceDetailVM
视图模型完全相同的 JS 对象上调用 JSON.stringify()
以将对象发送回服务器。 (我在一篇文章中读到在 JSON.stringify()
调用中使用替换器来手动处理字符串转换并调用 .toLocalDateString()
但随后 API 控制器无法将其识别为日期并拒绝调用。另外,我希望在后端将其存储为 UTC,以便日期始终在视图中本地化。)
const saveIncomeSource = async (incSrcDetailVM) => {
const transportObj = JSON.stringify(incSrcDetailVM);
const { data } = await axios.post(INC_SRC_ROUTE_PATH, transportObj, headerConfig);
await getIncSrcListItemVMs();
}
当对象回到我的 React 应用程序的状态时,我使用效果来更新本地状态。作为其中的一部分,我转换了服务器发送到 new Date()
的所有字符串日期。此过程将这些 UTC 日期字符串解释为当地时间而不是 UTC。
useEffect(() => {
const { account, toAccounts} = incSrcEditorVM;
if ((account.schedule
&& !(account.schedule instanceof ScheduleBase))
) {
account.schedule = ScheduleBase.clone(account.schedule);
}
setIncSrc(account)
setToAccounts(toAccounts);
setLoading(false);
return () => {
setLoading(true);
setToAccounts([...blankIncSrc.toAccounts]);
}
}, [incSrcEditorVM]);
export default class ScheduleBase {
scheduleID = 0;
frequency = 0;
occurrence_First;
occurrence_LastConfirmed = null;
occurrence_LastPlanned = null;
occurrence_Final = null;
dateTime_Created = new Date();
dateTime_Deactivated = null;
hasCustomTransactionTime = false;
isAutoConfirm = false;
constructor() {
this.occurrence_First = new Date(Date.now());
this.dateTime_Created = new Date(Date.now());
}
static clone(schedBaseShapedObj) {
const clone = new ScheduleBase();
clone.frequency = schedBaseShapedObj.frequency;
clone.scheduleID = schedBaseShapedObj.scheduleID;
clone.occurrence_First = new Date(schedBaseShapedObj.occurrence_First);
clone.occurrence_LastConfirmed = populateDate(schedBaseShapedObj.occurrence_LastConfirmed);
clone.occurrence_LastPlanned = populateDate(schedBaseShapedObj.occurrence_LastPlanned);
clone.occurrence_Final = populateDate(schedBaseShapedObj.occurrence_Final);
clone.dateTime_Created = new Date(schedBaseShapedObj.dateTime_Created);
clone.dateTime_Deactivated = populateDate(schedBaseShapedObj.dateTime_Deactivated);
clone.hasCustomTransactionTime = schedBaseShapedObj.hasCustomTransactionTime;
clone.isAutoConfirm = schedBaseShapedObj.isAutoConfirm;
return clone;
}
}
// **Fix goes in here**
function populateDate(nullableDate) {
return (nullableDate === null)
? null
: new Date(nullableDate + "Z");
}
我觉得这必须是一个足够普遍的问题,所以有更直接的方法可以做到这一点,而我只是想念它。
提前感谢所有花时间阅读本文的人。
对于坚持这个的人,答案就在这里:
添加“Z”以指示来自服务器的时间是 UTC 或 ZULU 让它停止尝试将服务器的 UTC 时间戳读取为本地时间。
更新:我已经在此处添加了修复程序,以供任何正在寻找其正常工作的前后示例的人使用。
我知道这有点重复,但我似乎一直在寻找部分答案,但没有找到完整的解决方案。我看到很多关于在后端以 UTC 格式存储日期的评论,我确实想这样做,但是 how/where 上没有真正的整体解决方案来处理过去几年内的翻译。
我知道 JS Date()
对象存储时区信息,然后当您在其上使用 JSON.stringify()
时,它会将其交换为 UTC 时间。这将时间提前 7 小时(对于我的语言环境),因此每次我保存日期时它都会提前 7 小时。我该如何解决?我确实希望将日期作为 UTC 存储在数据库中,但我不确定我需要在哪里执行转换才能将它从 UTC 返回到我的语言环境时间,当它 returns 到浏览器时。
我试图在下面包含整个代码路径,希望有人能指出我遗漏的内容。
首先,我有两个 EFCore5 类,我用它们以数据优先的方法构建我的数据库。一个income Source和一个Schedule,这些对象之间是一对一的关系。
public class Schedule
{
[Key]
public int ScheduleID { get; set; }
[Required]
public ScheduleFrequency Frequency { get; set; }
[DataType(DataType.Date)]
public DateTime Occurrence_First { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_LastConfirmed { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_LastPlanned { get; set; }
[DataType(DataType.Date)]
public DateTime? Occurrence_Final { get; set; }
public bool IsAutoConfirm { get; set; }
[DataType(DataType.Date)]
public DateTime DateTime_Created { get; set; }
[DataType(DataType.Date)]
public DateTime? DateTime_Deactivated { get; set; }
public bool HasCustomTransactionTime { get; set; }
[System.Text.Json.Serialization.JsonIgnore]
public IncomeSource IncomeSource { get; set; }
}
public class IncomeSource
{
[Key]
public int IncomeSourceID { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal ExpectedAmount { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal TotalFromSource { get; set; }
public int? DefaultToAccountID { get; set; }
public int ScheduleID { get; set; }
public BudgetorAccount Account { get; set; }
public Schedule Schedule { get; set; }
public int AccountID { get; set; }
[ForeignKey("DefaultToAccountID")]
public BudgetorAccount DefaultToAccount { get; set; }
}
public class BudgetorDbContext : DbContext
{
public BudgetorDbContext(DbContextOptions<BudgetorDbContext> options) : base (options)
{
}
public DbSet<BudgetorAccount> Accounts { get; set; }
public DbSet<IncomeSource> IncomeSources { get; set; }
public DbSet<Schedule> Schedules { get; set; }
}
API 控制器调用一个服务来转换to/from 视图模型并处理所有 CRUD 操作。该服务利用 EFCore 自动保存相关对象的能力来保存附加到 IncomeSource
Schedule
public class IncomeSourceDetailVM : AccountDetailVM
{
public int IncomeSourceId { get; set; }
public decimal ExpectedAmount { get; set; }
public decimal TotalFromSource { get; set; }
public int? DefaultToAccountID { get; set; }
public Schedule Schedule { get; set; }
public IncomeSourceDetailVM() : base(AccountType.IncomeSource)
{
this.Schedule = new Schedule()
{
Occurrence_First = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day)
};
}
}
public class BudgetorDataService : IBudgetorService
{
public async Task<IncomeSourceDetailVM> GetIncomeSourceDetailVM(int id)
{
var vm = await getIncomeSourceData()
.Where(inc => inc.AccountID == id)
.Select(incSrc => new IncomeSourceDetailVM()
{
IncomeSourceId = incSrc.IncomeSourceID,
ExpectedAmount = incSrc.ExpectedAmount,
AccountId = incSrc.AccountID,
AccountName = incSrc.Account.Name,
DateTime_Created = incSrc.Account.DateTime_Created,
DateTime_Deactivated = incSrc.Account.DateTime_Deactivated,
DefaultToAccountID = incSrc.DefaultToAccountID,
Schedule = incSrc.Schedule,
Notes = incSrc.Account.Notes,
})
.FirstOrDefaultAsync();
return vm;
}
public async Task<IncomeSourceDetailVM> CreateIncomeSource(IncomeSourceDetailVM incomeSourceToAdd)
{
if (incomeSourceToAdd == null)
{
throw new ArgumentNullException(nameof(incomeSourceToAdd));
}
try
{
BudgetorAccount newAcct = new BudgetorAccount()
{
AccountType = AccountType.IncomeSource,
DateTime_Created = DateTime.Now,
Name = incomeSourceToAdd.AccountName,
Notes = incomeSourceToAdd.Notes,
IsSystem = false
};
await _context.Accounts.AddAsync(newAcct);
await _context.SaveChangesAsync();
incomeSourceToAdd.AccountId = newAcct.AccountID;
incomeSourceToAdd.DateTime_Created = newAcct.DateTime_Created;
IncomeSource newIncSrc = new IncomeSource()
{
AccountID = newAcct.AccountID,
DefaultToAccountID = incomeSourceToAdd.DefaultToAccountID,
ExpectedAmount = incomeSourceToAdd.ExpectedAmount,
Schedule = incomeSourceToAdd.Schedule
};
await _context.IncomeSources.AddAsync(newIncSrc);
await _context.SaveChangesAsync();
incomeSourceToAdd.IncomeSourceId = newIncSrc.IncomeSourceID;
}
catch(Exception err)
{
throw err;
}
return incomeSourceToAdd;
}
}
[ApiController]
public class IncSrcController : ControllerBase
{
private readonly IBudgetorService _budgetorService;
public IncSrcController(IBudgetorService budgetorService)
{
this._budgetorService = budgetorService;
}
[HttpPost]
[Consumes("application/json")]
public async Task<ActionResult<IncomeSourceDetailVM>> PostIncSrc([FromBody] IncomeSourceDetailVM incSrcToAdd)
{
IncomeSourceDetailVM createdIncSrc = incSrcToAdd.AccountId == 0
? await this._budgetorService.CreateIncomeSource(incSrcToAdd)
: await this._budgetorService.UpdateIncomeSource(incSrcToAdd);
return StatusCode(201, createdIncSrc);
}
[HttpGet("{id}")]
public async Task<ActionResult<IncSrcManagementVM>> GetAccount(int? id)
{
IncSrcManagementVM result = new IncSrcManagementVM()
{
Account = (id == 0 || !id.HasValue)
? new IncomeSourceDetailVM()
: await this._budgetorService.GetIncomeSourceDetailVM(id.Value),
ToAccounts = await this._budgetorService.GetIncSrcToAccounts()
};
if (result.Account.DefaultToAccountID.HasValue)
{
result.ToAccounts
.Find(s => s.AccountId == result.Account.DefaultToAccountID)
.IsDefault = true;
}
return result;
}
}
在前端,我在形状与上述 C# 中的 IncomeSourceDetailVM
视图模型完全相同的 JS 对象上调用 JSON.stringify()
以将对象发送回服务器。 (我在一篇文章中读到在 JSON.stringify()
调用中使用替换器来手动处理字符串转换并调用 .toLocalDateString()
但随后 API 控制器无法将其识别为日期并拒绝调用。另外,我希望在后端将其存储为 UTC,以便日期始终在视图中本地化。)
const saveIncomeSource = async (incSrcDetailVM) => {
const transportObj = JSON.stringify(incSrcDetailVM);
const { data } = await axios.post(INC_SRC_ROUTE_PATH, transportObj, headerConfig);
await getIncSrcListItemVMs();
}
当对象回到我的 React 应用程序的状态时,我使用效果来更新本地状态。作为其中的一部分,我转换了服务器发送到 new Date()
的所有字符串日期。此过程将这些 UTC 日期字符串解释为当地时间而不是 UTC。
useEffect(() => {
const { account, toAccounts} = incSrcEditorVM;
if ((account.schedule
&& !(account.schedule instanceof ScheduleBase))
) {
account.schedule = ScheduleBase.clone(account.schedule);
}
setIncSrc(account)
setToAccounts(toAccounts);
setLoading(false);
return () => {
setLoading(true);
setToAccounts([...blankIncSrc.toAccounts]);
}
}, [incSrcEditorVM]);
export default class ScheduleBase {
scheduleID = 0;
frequency = 0;
occurrence_First;
occurrence_LastConfirmed = null;
occurrence_LastPlanned = null;
occurrence_Final = null;
dateTime_Created = new Date();
dateTime_Deactivated = null;
hasCustomTransactionTime = false;
isAutoConfirm = false;
constructor() {
this.occurrence_First = new Date(Date.now());
this.dateTime_Created = new Date(Date.now());
}
static clone(schedBaseShapedObj) {
const clone = new ScheduleBase();
clone.frequency = schedBaseShapedObj.frequency;
clone.scheduleID = schedBaseShapedObj.scheduleID;
clone.occurrence_First = new Date(schedBaseShapedObj.occurrence_First);
clone.occurrence_LastConfirmed = populateDate(schedBaseShapedObj.occurrence_LastConfirmed);
clone.occurrence_LastPlanned = populateDate(schedBaseShapedObj.occurrence_LastPlanned);
clone.occurrence_Final = populateDate(schedBaseShapedObj.occurrence_Final);
clone.dateTime_Created = new Date(schedBaseShapedObj.dateTime_Created);
clone.dateTime_Deactivated = populateDate(schedBaseShapedObj.dateTime_Deactivated);
clone.hasCustomTransactionTime = schedBaseShapedObj.hasCustomTransactionTime;
clone.isAutoConfirm = schedBaseShapedObj.isAutoConfirm;
return clone;
}
}
// **Fix goes in here**
function populateDate(nullableDate) {
return (nullableDate === null)
? null
: new Date(nullableDate + "Z");
}
我觉得这必须是一个足够普遍的问题,所以有更直接的方法可以做到这一点,而我只是想念它。
提前感谢所有花时间阅读本文的人。
对于坚持这个的人,答案就在这里:
添加“Z”以指示来自服务器的时间是 UTC 或 ZULU 让它停止尝试将服务器的 UTC 时间戳读取为本地时间。