Отображение даты сборки

В настоящее время у меня есть приложение, отображающее номер сборки в окне заголовка. Это хорошо, за исключением того, что это ничего не значит для большинства пользователей, которые хотят знать, есть ли у них последняя сборка - они обычно называют ее «сборкой прошлого четверга», а не сборкой 1.0.8.4321.

Вместо этого планируется указать дату сборки - например, «Приложение создано 21/10/2009».

Я изо всех сил пытаюсь найти программный способ вывести дату сборки в виде текстовой строки для использования таким образом.

В качестве номера сборки я использовал:

Assembly.GetExecutingAssembly().GetName().Version.ToString()

после определения того, как они возникли.

Я бы хотел что-то подобное для даты компиляции (и времени для бонусных баллов).

Указатели здесь очень ценятся (извините за каламбур, если уместно) или более аккуратные решения ...


person Mark Mayo    schedule 21.10.2009    source источник
comment
Я пробовал поставляемые способы получить данные сборки сборок, которые работают в простых сценариях, но если две сборки объединяются вместе, я получаю неправильное время сборки, это один час в будущем .. какие-либо предложения?   -  person    schedule 16.03.2011


Ответы (26)


Джефф Этвуд сказал несколько слов об этой проблеме в Определение даты сборки трудный путь.

Наиболее надежным методом оказывается получение метки времени компоновщика из PE-заголовок, встроенный в исполняемый файл - некоторый код C # (от Джо Спайви) для этого из комментариев к статье Джеффа:

public static DateTime GetLinkerTime(this Assembly assembly, TimeZoneInfo target = null)
{
    var filePath = assembly.Location;
    const int c_PeHeaderOffset = 60;
    const int c_LinkerTimestampOffset = 8;

    var buffer = new byte[2048];

    using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        stream.Read(buffer, 0, 2048);

    var offset = BitConverter.ToInt32(buffer, c_PeHeaderOffset);
    var secondsSince1970 = BitConverter.ToInt32(buffer, offset + c_LinkerTimestampOffset);
    var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    var linkTimeUtc = epoch.AddSeconds(secondsSince1970);

    var tz = target ?? TimeZoneInfo.Local;
    var localTime = TimeZoneInfo.ConvertTimeFromUtc(linkTimeUtc, tz);

    return localTime;
}

Пример использования:

var linkTimeLocal = Assembly.GetExecutingAssembly().GetLinkerTime();

ОБНОВЛЕНИЕ: метод работал для .Net Core 1.0, но перестал работать после выпуска .Net Core 1.1 (дает случайные годы в диапазоне 1900-2020)

person mdb    schedule 21.10.2009
comment
очень впечатляет, но я попробовал 3 строчку Джона ниже, и, конечно же, для моей системы и версий она, похоже, работает нормально. Однако, согласно статье, у этого метода есть некоторые ограничения, поэтому я голосую за вас обоих и принимаю ваш, чтобы будущие читатели могли извлечь выгоду из его надежности. Спасибо! - person Mark Mayo; 21.10.2009
comment
Я бы никогда не стал копаться в заголовке PE таким образом, просто чтобы получить информацию о версии сборки. У меня никогда не было проблем с тем, что номер сборки не обновлялся до настоящего времени, эта проблема ушла в прошлое. Поскольку вы смотрите на исполняемый файл как на необработанные байты, у вас нет гарантий, что заголовок PE не изменится в будущем или вообще не будет заголовком Windows PE (работает ли это в моно? Вероятно, да). И это единственная причина, по которой вам когда-либо понадобится. Помимо формата существует вероятная проблема с порядком байтов на XBOX360, с которой вы столкнетесь, когда кто-то попытается перенести этот код. - person John Leidegren; 20.02.2010
comment
Я немного изменил свой тон по этому поводу, я по-прежнему буду очень осторожен, копаясь в фактическом заголовке PE. Но насколько я могу судить, этот PE материал намного надежнее, чем использование номеров версий, кроме того, я не хочу назначать номера версий отдельно от даты сборки. - person John Leidegren; 01.10.2010
comment
Мне это нравится, и я использую его, но предпоследняя строка с .AddHours() довольно хакерская и (я думаю) не будет учитывать DST. Если вы хотите, чтобы это было по местному времени, вы должны вместо этого использовать очиститель dt.ToLocalTime();. Средняя часть также может быть значительно упрощена с помощью блока using(). - person JLRishe; 07.05.2013
comment
@JLRishe Согласен. Использование .AddHours () также не учитывает часовые пояса с долями часа. - person Daniel Curtis; 29.10.2013
comment
Вот это да. Странно, как вы не можете запросить это через какие-либо классы фреймворка. Однако проблема с UTC меня раздражает, тем более, что я хочу использовать ее для ведения журнала. Клиенты часто отправляют частичный файл журнала на инструменты, которые не могут реально использовать номера версий (поскольку это хост служб, который ведет журнал, но службы на нем обновляются), поэтому мне нужна дата сборки в нем, но кажется Тогда лучшим вариантом будет сохранение в формате UTC. - person Nyerguds; 10.06.2014
comment
более непрозрачный, чем ответ @ JohnLeidegren - person JJS; 29.07.2015
comment
Модернизировал этот ответ - person Chris Marisic; 22.01.2016
comment
Я не знаю, намеренно это или нет, но для ядра .net 1.1.1 это, похоже, недействительно (хотя работало в предыдущих версиях ядра .net). Это при условии, что у меня не было путешествия во времени в 1960-е (получение отрицательных эпох). - person KallDrexx; 14.03.2017
comment
Да, у меня это перестало работать и с ядром .net (1940-е, 1960-е и т. Д.) - person eoleary; 28.03.2017
comment
Поскольку это больше не работает в последнем ядре .net, я теперь использую задачи MSBUILD для записи текущего UTCNow в builddate.txt. Затем мой код загружает текст из этого файла - person KallDrexx; 25.04.2017
comment
Снова работает с .net core 2.x - person djunod; 02.11.2017
comment
Хотя использование заголовка PE сегодня может показаться хорошим вариантом, стоит отметить, что MS экспериментирует с детерминированными сборками (которые сделают этот заголовок бесполезным) и, возможно, даже делает его по умолчанию в будущих версиях компилятора C # (по уважительным причинам). Хорошее прочтение: blog.paranoidcoding.com/2016/04 / 05 / и вот ответ, связанный с .NET Core (TL; DR: это задумано): developercommunity.visualstudio.com/content/problem/35873/ - person Paweł Bulwan; 12.12.2017
comment
Похоже, что этот метод является деталью реализации, основанной на блоге Джареда Пэра: практика проверки метки времени сомнительна, но с учетом инструментов, которые это делали, был значительный риск обратной совместимости. Следовательно, мы перешли к текущему вычисленному значению и с тех пор не обнаружили никаких проблем. По общему признанию, похоже, что этот метод нанесет вред только .NET Core. - person jrh; 18.12.2017
comment
Это версия, которую я мог бы заставить работать в ASP.NET 4.7. - person MikeJ; 17.01.2018
comment
Для тех, кто обнаружил, что это больше не работает, проблема не в .NET Core. См. Мой ответ ниже о новых значениях параметров сборки по умолчанию, начиная с Visual Studio 15.4. - person Tom; 14.02.2018
comment
Обратите внимание, что размер, переданный в FileStream.Read, является максимальным, он не гарантирует, что что-либо было прочитано, и должен вызываться в цикле для обеспечения правильного поведения. - person Tim Sylvester; 19.06.2018
comment
Учитывая, что он не добавлен, и это принятый ответ. Это также обновляется, когда выполняется JIT-компиляция библиотеки DLL. Так что, если вы используете это на веб-сайте для отображения метки времени сборки, скорее всего, это будет неверно. - person Dave3of5; 23.11.2018
comment
ОБРАТИТЕ ВНИМАНИЕ: использование команд в предварительной сборке приведет к тому, что Visual Studio создаст файл .cmd со случайным именем, который будет заблокирован AVAST. Что-то вроде c: \ users \ username \ AppData \ Local \ Temp \ tmpbafc948d2ea741e3.exec.cmd. Этот файл имеет случайное имя и НЕ МОЖЕТ быть внесен в белый список с помощью подстановочного знака. - person Some_Yahoo; 03.02.2021
comment
Это не работает для dot net core ›1.1, которое сегодня является стандартом. Пожалуйста, рассмотрите решение Отиса Бурмана - person Christian Müller; 06.04.2021

Добавьте ниже в командную строку события перед сборкой:

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

Добавьте этот файл в качестве ресурса, теперь у вас есть строка BuildDate в ваших ресурсах.

Чтобы создать ресурсы, см. Как создавать и использовать ресурсы в .NET .

person Abdurrahim    schedule 19.03.2013
comment
Умный способ получить надежную дату сборки. Я выполнил сценарий (python) в событии перед сборкой, чтобы лучше контролировать форматирование даты сборки. - person bj0; 21.10.2013
comment
Я думаю, что это лучший способ, однако, если сборка не ваша собственная, нам, возможно, придется использовать решение @mdb. Я обязательно воспользуюсь вашим умным решением. - person Mubashar; 10.01.2014
comment
+1 от меня, просто и эффективно. Мне даже удалось получить значение из файла с помощью такой строки кода: String buildDate = ‹MyClassLibraryName› .Properties.Resources.BuildDate - person davidfrancis; 14.02.2014
comment
+1 К этому BTW. Так элегантно и просто. Однако я использовал другой формат даты и времени (ГГГГ-ММ-ДД ЧЧ: ММ: SS.MS), чтобы гарантировать синтаксический анализ в любой культуре. - person Jason D; 09.05.2014
comment
другой вариант - создать класс: (необходимо включить в проект после первой компиляции) - ›echo namespace My.app.namespace {public static class Build {public static string Timestamp =% DATE%% TIME% .Substring (0,16);}} ›$ (ProjectDir) \ BuildTimestamp.cs - - - -› затем можно вызвать его с помощью Build.Timestamp - person FabianSilva; 02.06.2014
comment
Это отличное решение. Единственная проблема заключается в том, что переменные командной строки% date% и% time% локализованы, поэтому вывод будет зависеть от языка Windows пользователя. - person V.S.; 23.07.2014
comment
+1, это лучший метод, чем чтение заголовков PE - потому что есть несколько сценариев, в которых это вообще не сработает (например, приложение Windows Phone) - person Matt Whitfield; 02.08.2014
comment
Пробовал и это. Хотя вывод отличается от среды к среде (в Windows, не заставляйте меня начинать сборку на Mac или Linux). - person Yves Schelpe; 18.02.2015
comment
Умный. Вы также можете использовать powershell, чтобы получить более точный контроль над форматом, например чтобы получить дату и время в формате UTC в формате ISO8601: powershell -Command ((Get-Date) .ToUniversalTime ()). ToString (\ s \) | Out-файл '$ (ProjectDir) Resources \ BuildDate.txt' - person dbruning; 09.10.2015
comment
Чтобы использовать это из бритвы в ASP.NET, убедитесь, что видимость ресурса public. - person galdin; 14.11.2015
comment
Это действительно должно быть UTC в нелокализованном варианте. - person Joey; 21.02.2018
comment
Я сделал это, но использовал Assets, потому что проще обрабатывать простые текстовые файлы (в то время как файл ресурсов должен быть в определенном формате) - person Jimbot; 24.04.2018
comment
Мне пришлось выполнить echo% 25date% 25% 25time% 25 ›$ (ProjectDir) Resources \ BuildDate.txt, чтобы заставить его работать - см. developercommunity.visualstudio.com/content/problem/237752/ - person David Christopher Reynolds; 20.03.2019
comment
Я использую следующий код: lblVersion.Text = String.Format("Version: {0} built on {1}", Assembly.GetEntryAssembly().GetName().Version, Properties.Resources.BuildDate); Код работает правильно, но при изменении свойства Enabled для нескольких выбранных кнопок в режиме разработки я получаю: Недопустимое значение свойства. Не удалось найти файл C: \ Users \ ... \ Resources \ BuildDate.txt.txt. (Обратите внимание на двойное расширение.) Я использую Visual Studio Community 2015 версии 14.0.25431.01 с обновлением 3 (.NET Framework версии 4.8.04084) - person Derek Johnson; 07.01.2021

Путь

Как указано @ c00000fd в комментариях. Microsoft меняет это. И хотя многие люди не используют последнюю версию своего компилятора, я подозреваю, что это изменение делает этот подход, несомненно, плохим. И хотя это забавное упражнение, я бы порекомендовал людям просто встроить дату сборки в свой двоичный файл с помощью любых других средств, необходимых, если важно отслеживать дату сборки самого двоичного файла.

Это можно сделать с помощью некоторой тривиальной генерации кода, которая, вероятно, уже является первым шагом в вашем сценарии сборки. Это и тот факт, что инструменты ALM / Build / DevOps очень помогают в этом, и их следует предпочесть всему остальному.

Я оставляю здесь остальную часть ответа только для исторических целей.

Новый способ

Я передумал по этому поводу и сейчас использую этот трюк, чтобы получить правильную дату сборки.

#region Gets the build date and time (by reading the COFF header)

// http://msdn.microsoft.com/en-us/library/ms680313

struct _IMAGE_FILE_HEADER
{
    public ushort Machine;
    public ushort NumberOfSections;
    public uint TimeDateStamp;
    public uint PointerToSymbolTable;
    public uint NumberOfSymbols;
    public ushort SizeOfOptionalHeader;
    public ushort Characteristics;
};

static DateTime GetBuildDateTime(Assembly assembly)
{
    var path = assembly.GetName().CodeBase;
    if (File.Exists(path))
    {
        var buffer = new byte[Math.Max(Marshal.SizeOf(typeof(_IMAGE_FILE_HEADER)), 4)];
        using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
        {
            fileStream.Position = 0x3C;
            fileStream.Read(buffer, 0, 4);
            fileStream.Position = BitConverter.ToUInt32(buffer, 0); // COFF header offset
            fileStream.Read(buffer, 0, 4); // "PE\0\0"
            fileStream.Read(buffer, 0, buffer.Length);
        }
        var pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        try
        {
            var coffHeader = (_IMAGE_FILE_HEADER)Marshal.PtrToStructure(pinnedBuffer.AddrOfPinnedObject(), typeof(_IMAGE_FILE_HEADER));

            return TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1) + new TimeSpan(coffHeader.TimeDateStamp * TimeSpan.TicksPerSecond));
        }
        finally
        {
            pinnedBuffer.Free();
        }
    }
    return new DateTime();
}

#endregion

По старому

Ну, а как вы генерируете номера сборки? Visual Studio (или компилятор C #) фактически предоставляет автоматические номера сборки и версии, если вы измените атрибут AssemblyVersion, например, на 1.0.*

Что произойдет, так это то, что сборка будет равна количеству дней с 1 января 2000 года по местному времени, а для ревизии - количеству секунд, прошедших с полуночи по местному времени, разделенному на 2.

см. Материалы сообщества, номера автоматической сборки и версии

например AssemblyInfo.cs

[assembly: AssemblyVersion("1.0.*")] // important: use wildcard for build and revision numbers!

SampleCode.cs

var version = Assembly.GetEntryAssembly().GetName().Version;
var buildDateTime = new DateTime(2000, 1, 1).Add(new TimeSpan(
TimeSpan.TicksPerDay * version.Build + // days since 1 January 2000
TimeSpan.TicksPerSecond * 2 * version.Revision)); // seconds since midnight, (multiply by 2 to get original)
person John Leidegren    schedule 21.10.2009
comment
Отлично, это был лишь ограниченный короткий ответ, который мне нужен, и он отлично работает. Однако я считаю, что для полноты приведенный выше ответ mdb не позволит другим стрелять себе в ногу, поэтому принять его. Отдал вам голос - спасибо! - person Mark Mayo; 21.10.2009
comment
Кажется, что контент сообщества удален ... Как мы можем быть уверены, что MS не изменит алгоритм? :) Есть какие-нибудь официальные документы по этому поводу, ребята? - person Alex from Jitbit; 29.06.2010
comment
Этот метод дает мне отметку времени, которая отключается на один час. Я предполагаю, что это из-за перехода на летнее время или, возможно, из-за моего часового пояса i CET, то есть +1. Я пробовал создать новый DateTime с DateTimeKind.Local и DateTimeKind.Utc, но это не помогает. - person Jan Aagaard; 30.09.2010
comment
Да, в конце концов, я стал меньше думать об этом подходе. Копаться в заголовке PE намного проще, и вы получаете метку времени в формате UTC, с которой можете делать правильные вещи. Я использую номер версии для других вещей, кроме определения даты сборки сейчас в днях. Однако я написал свой код немного иначе, я бы не предполагал, что временная метка каждый раз обнаруживается с определенным смещением. - person John Leidegren; 01.10.2010
comment
Я только что добавил один час, если TimeZone.CurrentTimeZone.IsDaylightSavingTime(buildDateTime) == true - person e4rthdog; 21.06.2013
comment
К сожалению, я использовал этот подход без тщательной проверки, и он кусает нас в продакшене. Проблема в том, что при запуске JIT-компилятора информация заголовка PE изменяется. Отсюда и голос против. Теперь я собираюсь провести ненужное «исследование», чтобы объяснить, почему мы видим дату установки как дату сборки. - person Jason D; 09.05.2014
comment
@JasonD В какой вселенной ваша проблема каким-то образом стала моей проблемой? Как вы оправдываете отрицательный голос просто потому, что столкнулись с проблемой, которую эта реализация не приняла во внимание. Вы получили это бесплатно и плохо протестировали. Также почему вы думаете, что заголовок переписывается JIT-компилятором? Вы читаете эту информацию из памяти процесса или из файла? - person John Leidegren; 11.05.2014
comment
Потому что дата сборки PE-заголовка не совпадает с фактической датой сборки. Заголовок вопроса: Отображение даты сборки. У вас не было никаких предупреждений о JIT. Отсюда и голос против. - person Jason D; 10.06.2014
comment
Новое решение. Глупый вопрос, а куда это девается? В 1_? Кроме того, что это за тип CoffHeader? - person gamut; 06.04.2016
comment
Я заметил, что если вы работаете в веб-приложении, свойство .Codebase выглядит как URL-адрес (file: // c: /path/to/binary.dll). Это приводит к сбою вызова File.Exists. Использование assembly.Location вместо свойства CodeBase решило проблему для меня. - person mdryden; 06.12.2017
comment
@mdryden были разные попытки исправить это, отредактировав мой ответ. В конечном итоге это привело к образцу кода, который даже не компилировался. Я откатывал этот ответ пару раз, потому что считаю, что это чрезмерное редактирование, вы можете дать свой собственный ответ на вопрос, если хотите. С учетом сказанного, вы можете посмотреть историю изменений для этого ответа, поскольку в нем может быть пара подсказок, и если вы действительно готовы, предоставьте свой собственный ответ. - person John Leidegren; 07.12.2017
comment
@JohnLeidegren Спасибо, Джон. Я не думаю, что предлагаю это изменить, я уверен, что это изменение не сработает для кого-то другого, как это обычно бывает с этими вещами. Я просто подумал, что следующий комментарий может помочь кому-то другому, если они столкнутся с той же проблемой. - person mdryden; 11.12.2017
comment
Новый способ сработал для меня, мне оставалось только построить путь, используя URI. Танки. - person Thadeu Antonio Ferreira Melo; 24.01.2018
comment
@JohnLeidegren: не полагайтесь для этого на заголовок Windows PE. Начиная с Windows 10 и воспроизводимые сборки, поле IMAGE_FILE_HEADER::TimeDateStamp устанавливается на случайное число и больше не является меткой времени. - person c00000fd; 13.02.2019
comment
Исправление для пути: строка codeBase = assembly.CodeBase; UriBuilder uri = новый UriBuilder (codeBase); строка path = Uri.UnescapeDataString (uri.Path); - person klm_; 12.03.2019
comment
Обратите внимание, что 32-битный TimeDateStamp будет переполняться 19 января 2037 года, как и все 32-битные временные метки эпохи Unix, встроенные в двоичные файлы. - person David R Tribble; 09.07.2019

Добавьте ниже в командную строку события перед сборкой:

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

Добавьте этот файл в качестве ресурса, теперь у вас есть строка BuildDate в ваших ресурсах.

После вставки файла в ресурс (как общедоступный текстовый файл) я получил к нему доступ через

string strCompTime = Properties.Resources.BuildDate;

Чтобы создать ресурсы, см. Как создавать и использовать ресурсы в .NET .

person brewmanz    schedule 04.03.2014
comment
@DavidGorsline - разметка комментария была правильной, поскольку он цитирует этот другой ответ. У меня недостаточно репутации, чтобы откатить ваше изменение, иначе я бы сделал это сам. - person Wai Ha Lee; 07.07.2015
comment
@Wai Ha Lee - а) ответ, который вы цитируете, не дает кода для фактического получения даты / времени компиляции. б) в то время у меня не было достаточно репутации, чтобы добавить комментарий к этому ответу (что я бы сделал) только для публикации. так в) Я опубликовал полный ответ, чтобы люди могли получить все подробности в одной области .. - person brewmanz; 08.07.2015
comment
Если вы видите Úte% вместо% date%, проверьте здесь: developercommunity.visualstudio.com/content/problem/237752/ Вкратце, сделайте следующее: echo% 25date% 25% 25time% 25 - person Qodex; 24.03.2019

Один из подходов, о котором я удивлен, что никто еще не упомянул, - это использование текстовых шаблонов T4 для генерации кода.

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ output extension=".g.cs" #>
using System;
namespace Foo.Bar
{
    public static partial class Constants
    {
        public static DateTime CompilationTimestampUtc { get { return new DateTime(<# Write(DateTime.UtcNow.Ticks.ToString()); #>L, DateTimeKind.Utc); } }
    }
}

Плюсы:

  • Независимый от локали
  • Позволяет гораздо больше, чем просто время компиляции

Минусы:

person Peter Taylor    schedule 19.10.2016
comment
Итак, это лучший ответ. Осталось 324 балла, прежде чем он станет самым популярным ответом :). Stackoverflow нужен способ показать самого быстрого альпиниста. - person pauldendulk; 04.04.2018
comment
@pauldendulk, не очень поможет, потому что ответ, получивший наибольшее количество голосов, и принятый ответ почти всегда набирают голоса быстрее всего. Принятый ответ на этот вопрос имеет + 60 / -2, так как я опубликовал этот ответ. - person Peter Taylor; 04.04.2018
comment
Я считаю, что вам нужно добавить .ToString () к своим Ticks (в противном случае я получаю ошибку компиляции). Тем не менее, здесь я столкнулся с крутой кривой обучения, не могли бы вы показать, как ИСПОЛЬЗОВАТЬ это в основной программе? - person Andy; 30.01.2019
comment
@ Энди, ты прав насчет ToString (). Использование всего Constants.CompilationTimestampUtc. Если VS не создает файл C # с классом, вам нужно выяснить, как заставить его это сделать, но ответ зависит (по крайней мере) от версии VS и типа файла csproj, поэтому он слишком много деталей для этого поста. - person Peter Taylor; 30.01.2019
comment
В случае, если другие задаются вопросом, вот что потребовалось, чтобы заставить его работать на VS 2017: мне пришлось сделать это шаблоном времени разработки T4 (мне потребовалось время, чтобы понять, я сначала добавил шаблон препроцессора). Мне также пришлось включить эту сборку: Microsoft.VisualStudio.TextTemplating.Interfaces.10.0 в качестве ссылки на проект. Наконец, мой шаблон должен был включать using System; перед пространством имен, иначе ссылка на DateTime не удалась. - person Andy; 30.01.2019
comment
Извините, я мог ошибаться, говоря о шаблоне дизайна и препроцессоре. Итак, вопрос: я использую Custom Tool: TextTemplatingPreProcessor, но выходной файл не обновляется при каждой сборке. Мне не хватает чего-то еще, чтобы заставить его обновляться при каждой компиляции? - person Andy; 30.01.2019
comment
Все заработало. Убедитесь, что вы установили пакет NuGet Clarius.TransformOnBuild, на который есть ссылка в ответе (Duh), и в конце концов, шаблон должен использовать настраиваемый инструмент TextTemplatingPreProcessor ... Теперь работает. - person Andy; 30.01.2019
comment
Я большой поклонник T4, но он не подходит для этого варианта использования. Неловко настраивать генерацию во время компиляции в проектах фреймворка и почти невозможно в .Net Core с новой системой SDK csproj. Мне нужна только дата сборки, поэтому более простой и надежный WriteLinesToFile подход к задаче MSBuild лучше: stackoverflow.com/a/50905092/418362 - person Artfunkel; 16.04.2020

Здесь много отличных ответов, но я чувствую, что могу добавить свой собственный из-за простоты, производительности (по сравнению с решениями, связанными с ресурсами) кросс-платформы (также работает с Net Core) и избегания любых сторонних инструментов. Просто добавьте эту цель msbuild в файл csproj.

<Target Name="Date" BeforeTargets="BeforeBuild">
    <WriteLinesToFile File="$(IntermediateOutputPath)gen.cs" Lines="static partial class Builtin { public static long CompileTime = $([System.DateTime]::UtcNow.Ticks) %3B }" Overwrite="true" />
    <ItemGroup>
        <Compile Include="$(IntermediateOutputPath)gen.cs" />
    </ItemGroup>
</Target>

и теперь у вас есть Builtin.CompileTime в этом проекте, например:

var compileTime = new DateTime(Builtin.CompileTime, DateTimeKind.Utc);

ReSharper это не понравится. Вы можете игнорировать его или добавить в проект частичный класс, но это все равно работает.

UPD: в настоящее время ReSharper имеет параметр на первой странице параметров: доступ к MSBuild, получение данных из MSBuild после каждой компиляции. Это помогает увидеть сгенерированный код.

person Dmitry Gusarov    schedule 18.06.2018
comment
Я могу строить с этим и разрабатывать локально (запускать веб-сайты) в ASP.NET Core 2.1, но публикация веб-развертывания из VS 2017 завершается с ошибкой. Имя «Встроенный» не существует в текущем контексте. ДОПОЛНЕНИЕ: если я открываю Builtin.CompileTime из представления Razor. - person Jeremy Cook; 05.11.2018
comment
В этом случае, я думаю, вам просто нужен BeforeTargets="RazorCoreCompile", но только пока он находится в том же проекте - person Dmitry Gusarov; 04.09.2019
comment
круто, а как же ссылаться на сгенерированный объект? мне кажется, что в ответе отсутствует ключевая часть ... - person Matteo; 12.03.2020
comment
@Matteo, как упоминалось в ответе, вы можете использовать Builtin.CompileTime или new DateTime (Builtin.CompileTime, DateTimeKind.Utc). Visual Studio IntelliSense способна это сразу увидеть. Старый ReSharper может жаловаться во время разработки, но похоже, что они исправили это в новых версиях. clip2net.com/s/46rgaaO - person Dmitry Gusarov; 14.03.2020
comment
Я использовал эту версию, поэтому нет необходимости в дополнительном коде для получения даты. Также resharper не жалуется на последнюю версию. ‹WriteLinesToFile File = $ (IntermediateOutputPath) BuildInfo.cs Lines = с использованием внутреннего статического частичного класса System% 3B BuildInfo {public static long DateBuiltTicks = $ ([System.DateTime] :: UtcNow.Ticks)% 3B public static DateTime DateBuilt =› новый DateTime (DateBuiltTicks, DateTimeKind.Utc)% 3B} Overwrite = true / › - person Softlion; 11.05.2020
comment
Мне пришлось изменить BeforeTargets = CoreCompile на BeforeTargets = BeforeBuild, чтобы устранить ошибку «Имя x не существует». - person John Thoits; 01.09.2020

Что касается метода извлечения информации о дате / версии сборки из байтов PE-заголовка сборки, Microsoft изменила параметры сборки по умолчанию, начиная с Visual Studio 15.4. Новое значение по умолчанию включает в себя детерминированную компиляцию, благодаря которой действительная временная метка и автоматически увеличивающиеся номера версий остались в прошлом. Поле отметки времени все еще присутствует, но оно заполняется постоянным значением, которое является хешем чего-то или другого, но не указанием времени сборки.

Подробная информация здесь

Для тех, кто предпочитает полезную метку времени детерминированной компиляции, есть способ переопределить новое значение по умолчанию. Вы можете включить тег в файл .csproj интересующей сборки следующим образом:

  <PropertyGroup>
      ...
      <Deterministic>false</Deterministic>
  </PropertyGroup>

Обновление: я поддерживаю решение текстового шаблона T4, описанное в другом ответе здесь. Я использовал его, чтобы чисто решить мою проблему, не теряя преимуществ детерминированной компиляции. Одно из предостережений заключается в том, что Visual Studio запускает компилятор T4 только при сохранении файла .tt, а не во время сборки. Это может быть неудобно, если вы исключите результат .cs из системы управления версиями (поскольку вы ожидаете, что он будет сгенерирован), а другой разработчик проверяет код. Без повторного сохранения у них не будет файла .cs. На nuget есть пакет (я думаю, это AutoT4), который делает компиляцию T4 частью каждой сборки. Я еще не сталкивался с решением этой проблемы во время производственного развертывания, но я ожидаю, что что-то подобное поможет исправить это.

person Tom    schedule 04.01.2018
comment
Это решило мою проблему в sln, который использует самый старый ответ. - person pauldendulk; 04.04.2018
comment
Ваше предостережение относительно T4 совершенно справедливо, но обратите внимание, что оно уже присутствует в моем ответе. - person Peter Taylor; 04.04.2018

Для проектов .NET Core я адаптировал ответ Postlagerkarte, чтобы обновить поле авторских прав сборки с датой сборки.

Непосредственно редактировать csproj

Следующее может быть добавлено непосредственно к первому PropertyGroup в csproj:

<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>

Альтернатива: Свойства проекта Visual Studio

Или вставьте внутреннее выражение непосредственно в поле Copyright в разделе Package свойств проекта в Visual Studio:

Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))

Это может немного сбивать с толку, потому что Visual Studio оценит выражение и отобразит текущее значение в окне, но также обновит файл проекта соответствующим образом «за кулисами».

В рамках всего решения через Directory.Build.props

Вы можете поместить указанный выше элемент <Copyright> в файл Directory.Build.props в корне вашего решения и автоматически применить его ко всем проектам в каталоге, предполагая, что каждый проект не предоставляет собственное значение авторских прав.

<Project>
 <PropertyGroup>
   <Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>
 </PropertyGroup>
</Project>

Directory.Build.props: Настройте свою сборку

Выход

Пример выражения предоставит вам такие авторские права:

Copyright © 2018 Travis Troyer (2018-05-30T14:46:23)

Retrieval

Вы можете просмотреть информацию об авторских правах в свойствах файла в Windows или получить ее во время выполнения:

var version = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location);

Console.WriteLine(version.LegalCopyright);
person Travis Troyer    schedule 30.05.2018

Я просто новичок в C #, поэтому, возможно, мой ответ звучит глупо - я показываю дату сборки с даты последней записи исполняемого файла:

string w_file = "MyProgram.exe"; 
string w_directory = Directory.GetCurrentDirectory();

DateTime c3 =  File.GetLastWriteTime(System.IO.Path.Combine(w_directory, w_file));
RTB_info.AppendText("Program created at: " + c3.ToString());

Я попытался использовать метод File.GetCreationTime, но получил странные результаты: дата команды была 2012-05-29, но дата в проводнике Windows показала 2012-05-23. После поиска этого несоответствия я обнаружил, что файл, вероятно, был создан 23 мая 2012 г. (как показано в проводнике Windows), но скопирован в текущую папку 29 мая 2012 г. (как показано командой File.GetCreationTime), поэтому на всякий случай я использую команду File.GetLastWriteTime.

Залек

person Zalek Bloom    schedule 15.06.2012
comment
Я не уверен, что это пуленепробиваемое копирование исполняемого файла на диски / компьютеры / сети. - person Stealth Rabbi; 01.11.2013
comment
это первое, что приходит в голову, но вы знаете, что это ненадежно, есть много программного обеспечения, используемого для перемещения файлов по сети, которое не обновляет атрибуты после загрузки, я бы пошел с ответом @ Abdurrahim. - person Mubashar; 10.01.2014
comment
Я знаю, что это старый, но я только что обнаружил с помощью некоторого похожего кода, что процесс INSTALL (по крайней мере, при использовании clickonce) обновляет время файла сборки. Не очень полезно. Однако не уверен, что это применимо к этому решению. - person bobwki; 21.08.2018
comment
Вероятно, вам действительно нужен LastWriteTime, поскольку он точно отражает время фактического обновления исполняемого файла. - person David R Tribble; 09.07.2019
comment
Извините, но время записи исполняемого файла не является надежным показателем времени сборки. Отметка времени файла может быть переписана из-за всякого рода вещей, которые находятся вне вашей сферы влияния. - person Tom; 05.09.2019
comment
У меня не получилось. Возвращает дату установки файла на новый компьютер. - person Ray White; 01.07.2021

В 2018 году некоторые из вышеперечисленных решений больше не работают или не работают с .NET Core.

Я использую следующий подход, который прост и подходит для моего проекта .NET Core 2.0.

Добавьте следующее в свой .csproj внутри PropertyGroup:

    <Today>$([System.DateTime]::Now)</Today>

Это определяет PropertyFunction, к которой вы можете получить доступ в своей команде предварительной сборки .

Ваша предварительная сборка выглядит так

echo $(today) > $(ProjectDir)BuildTimeStamp.txt

Установите для свойства BuildTimeStamp.txt значение Встроенный ресурс.

Теперь вы можете прочитать отметку времени вот так

public static class BuildTimeStamp
    {
        public static string GetTimestamp()
        {
            var assembly = Assembly.GetEntryAssembly(); 

            var stream = assembly.GetManifestResourceStream("NamespaceGoesHere.BuildTimeStamp.txt");

            using (var reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }
    }
person Postlagerkarte    schedule 12.01.2018
comment
Простое создание этого BuildTimeStamp.txt из событий перед сборкой с использованием команд пакетного сценария также работает. Обратите внимание, что вы допустили ошибку: вы должны заключить цель в кавычки (например, "$(ProjectDir)BuildTimeStamp.txt"), иначе она сломается, если в именах папок есть пробелы. - person Nyerguds; 18.06.2018
comment
Возможно, имеет смысл использовать формат времени, инвариантный для культуры. Вот так: $([System.DateTime]::Now.tostring("MM/dd/yyyy HH:mm:ss")) вместо $([System.DateTime]::Now) - person Ivan Kochurkin; 07.08.2018
comment
См. stackoverflow.com/a/11336754/4675770, чтобы избавиться от символа новой строки, созданного командой echo, чтобы txt имеет одну строку вместо двух. - person Jinjinov; 04.08.2020

Вышеупомянутый метод можно настроить для сборок, уже загруженных в процессе, используя изображение файла в памяти (в отличие от повторного чтения его из хранилища):

using System;
using System.Runtime.InteropServices;
using Assembly = System.Reflection.Assembly;

static class Utils
{
    public static DateTime GetLinkerDateTime(this Assembly assembly, TimeZoneInfo tzi = null)
    {
        // Constants related to the Windows PE file format.
        const int PE_HEADER_OFFSET = 60;
        const int LINKER_TIMESTAMP_OFFSET = 8;

        // Discover the base memory address where our assembly is loaded
        var entryModule = assembly.ManifestModule;
        var hMod = Marshal.GetHINSTANCE(entryModule);
        if (hMod == IntPtr.Zero - 1) throw new Exception("Failed to get HINSTANCE.");

        // Read the linker timestamp
        var offset = Marshal.ReadInt32(hMod, PE_HEADER_OFFSET);
        var secondsSince1970 = Marshal.ReadInt32(hMod, offset + LINKER_TIMESTAMP_OFFSET);

        // Convert the timestamp to a DateTime
        var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var linkTimeUtc = epoch.AddSeconds(secondsSince1970);
        var dt = TimeZoneInfo.ConvertTimeFromUtc(linkTimeUtc, tzi ?? TimeZoneInfo.Local);
        return dt;
    }
}
person tcostin    schedule 13.06.2017
comment
Этот отлично работает даже для фреймворка 4.7. Использование: Utils.GetLinkerDateTime (Assembly.GetExecutingAssembly (), null)) - person real_yggdrasil; 23.01.2018
comment
Это работает при сборке Debug-release, но убивает мое приложение (без исключения) при сборке как Release. К сожалению, я понятия не имею, почему. Я использую Visual Studio 2019 в Windows 10 20H2. - person Betaminos; 23.10.2020

Для тех, кому нужно время компиляции в Windows 8 / Windows Phone 8:

    public static async Task<DateTimeOffset?> RetrieveLinkerTimestamp(Assembly assembly)
    {
        var pkg = Windows.ApplicationModel.Package.Current;
        if (null == pkg)
        {
            return null;
        }

        var assemblyFile = await pkg.InstalledLocation.GetFileAsync(assembly.ManifestModule.Name);
        if (null == assemblyFile)
        {
            return null;
        }

        using (var stream = await assemblyFile.OpenSequentialReadAsync())
        {
            using (var reader = new DataReader(stream))
            {
                const int PeHeaderOffset = 60;
                const int LinkerTimestampOffset = 8;

                //read first 2048 bytes from the assembly file.
                byte[] b = new byte[2048];
                await reader.LoadAsync((uint)b.Length);
                reader.ReadBytes(b);
                reader.DetachStream();

                //get the pe header offset
                int i = System.BitConverter.ToInt32(b, PeHeaderOffset);

                //read the linker timestamp from the PE header
                int secondsSince1970 = System.BitConverter.ToInt32(b, i + LinkerTimestampOffset);

                var dt = new DateTimeOffset(1970, 1, 1, 0, 0, 0, DateTimeOffset.Now.Offset) + DateTimeOffset.Now.Offset;
                return dt.AddSeconds(secondsSince1970);
            }
        }
    }

Для тех, кому нужно время компиляции в Windows Phone 7:

    public static async Task<DateTimeOffset?> RetrieveLinkerTimestampAsync(Assembly assembly)
    {
        const int PeHeaderOffset = 60;
        const int LinkerTimestampOffset = 8;            
        byte[] b = new byte[2048];

        try
        {
            var rs = Application.GetResourceStream(new Uri(assembly.ManifestModule.Name, UriKind.Relative));
            using (var s = rs.Stream)
            {
                var asyncResult = s.BeginRead(b, 0, b.Length, null, null);
                int bytesRead = await Task.Factory.FromAsync<int>(asyncResult, s.EndRead);
            }
        }
        catch (System.IO.IOException)
        {
            return null;
        }

        int i = System.BitConverter.ToInt32(b, PeHeaderOffset);
        int secondsSince1970 = System.BitConverter.ToInt32(b, i + LinkerTimestampOffset);
        var dt = new DateTimeOffset(1970, 1, 1, 0, 0, 0, DateTimeOffset.Now.Offset) + DateTimeOffset.Now.Offset;
        dt = dt.AddSeconds(secondsSince1970);
        return dt;
    }

ПРИМЕЧАНИЕ. Во всех случаях вы работаете в песочнице, поэтому вы сможете узнать время компиляции только тех сборок, которые вы развертываете с помощью своего приложения. (т.е. это не будет работать ни с чем в GAC).

person Matt Dotson    schedule 21.08.2013
comment
Вот как получить сборку в WP 8.1: var assembly = typeof (AnyTypeInYourAssembly).GetTypeInfo().Assembly; - person André Fiedler; 01.09.2015
comment
Что, если вы хотите запустить свой код в обеих системах? - применим ли один из этих методов для обеих платформ? - person bvdb; 10.02.2017

Я просто:

File.GetCreationTime(GetType().Assembly.Location)
person Rui Santos    schedule 31.01.2019
comment
Интересно, что при запуске из отладки "истинной" датой является GetLastAccessTime (). - person balint; 07.02.2019
comment
Обратите внимание: вы добавляете using System.IO; и помещаете его в конструктор класса, поэтому GetType() работает с экземпляром. - person phyatt; 21.04.2021

Вариант, который здесь не обсуждается, - это вставить свои собственные данные в AssemblyInfo.cs, поле «AssemblyInformationalVersion» кажется подходящим - у нас есть несколько проектов, в которых мы делали что-то похожее на этапе сборки (однако я не совсем доволен так, как это работает, поэтому не очень хочу воспроизводить то, что у нас есть).

На эту тему есть статья о codeproject: http://www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx

person Murph    schedule 21.10.2009

Мне нужно было универсальное решение, которое работало бы с проектом NETStandard на любой платформе (iOS, Android и Windows). Для этого я решил автоматически сгенерировать файл CS с помощью сценария PowerShell. Вот сценарий PowerShell:

param($outputFile="BuildDate.cs")

$buildDate = Get-Date -date (Get-Date).ToUniversalTime() -Format o
$class = 
"using System;
using System.Globalization;

namespace MyNamespace
{
    public static class BuildDate
    {
        public const string BuildDateString = `"$buildDate`";
        public static readonly DateTime BuildDateUtc = DateTime.Parse(BuildDateString, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
    }
}"

Set-Content -Path $outputFile -Value $class

Сохраните файл PowerScript как GenBuildDate.ps1 и добавьте его в свой проект. Наконец, добавьте следующую строку в ваше событие Pre-Build:

powershell -File $(ProjectDir)GenBuildDate.ps1 -outputFile $(ProjectDir)BuildDate.cs

Убедитесь, что BuildDate.cs включен в ваш проект. Работает как чемпион на любой ОС!

person David Tosi    schedule 21.02.2018
comment
Вы также можете использовать это, чтобы получить номер версии SVN с помощью инструмента командной строки svn. Я сделал что-то подобное с этим. - person user169771; 05.04.2018

Другой подход, дружественный к PCL, заключался бы в использовании встроенной задачи MSBuild для замены времени сборки в строку, возвращаемую свойством в приложении. Мы успешно используем этот подход в приложении, в котором есть проекты Xamarin.Forms, Xamarin.Android и Xamarin.iOS.

РЕДАКТИРОВАТЬ:

Упрощается за счет перемещения всей логики в файл SetBuildDate.targets и использования Regex вместо простой замены строки, чтобы файл можно было изменять при каждой сборке без «сброса».

Определение встроенной задачи MSBuild (в этом примере сохраняется в файле SetBuildDate.targets, локальном для проекта Xamarin.Forms):

<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="12.0">

  <UsingTask TaskName="SetBuildDate" TaskFactory="CodeTaskFactory" 
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
    <ParameterGroup>
      <FilePath ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Code Type="Fragment" Language="cs"><![CDATA[

        DateTime now = DateTime.UtcNow;
        string buildDate = now.ToString("F");
        string replacement = string.Format("BuildDate => \"{0}\"", buildDate);
        string pattern = @"BuildDate => ""([^""]*)""";
        string content = File.ReadAllText(FilePath);
        System.Text.RegularExpressions.Regex rgx = new System.Text.RegularExpressions.Regex(pattern);
        content = rgx.Replace(content, replacement);
        File.WriteAllText(FilePath, content);
        File.SetLastWriteTimeUtc(FilePath, now);

   ]]></Code>
    </Task>
  </UsingTask>

</Project>

Вызов указанной выше встроенной задачи в файле csproj Xamarin.Forms в целевом BeforeBuild:

  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.  -->
  <Import Project="SetBuildDate.targets" />
  <Target Name="BeforeBuild">
    <SetBuildDate FilePath="$(MSBuildProjectDirectory)\BuildMetadata.cs" />
  </Target>

Свойство FilePath установлено в файл BuildMetadata.cs в проекте Xamarin.Forms, который содержит простой класс со строковым свойством BuildDate, в которое будет подставлено время сборки:

public class BuildMetadata
{
    public static string BuildDate => "This can be any arbitrary string";
}

Добавьте этот файл BuildMetadata.cs в проект. Он будет изменяться при каждой сборке, но таким образом, чтобы допускать повторные сборки (повторяющиеся замены), поэтому вы можете включать или опускать его в системе управления версиями по желанию.

person Mark Larter    schedule 09.05.2016

Вы можете использовать этот проект: https://github.com/dwcullop/BuildInfo

Он использует T4 для автоматизации отметки времени даты сборки. Существует несколько версий (разных веток), включая ту, которая дает вам Git Hash текущей проверенной ветки, если вам нравятся подобные вещи.

Раскрытие информации: я написал модуль.

person Darrin Cullop    schedule 22.08.2018

Небольшое обновление ответа Джона «Новый путь».

Вам нужно построить путь вместо использования строки CodeBase при работе с ASP.NET/MVC

    var codeBase = assembly.GetName().CodeBase;
    UriBuilder uri = new UriBuilder(codeBase);
    string path = Uri.UnescapeDataString(uri.Path);
person Thadeu Antonio Ferreira Melo    schedule 24.01.2018

Вы можете использовать событие после сборки проекта, чтобы записать текстовый файл в целевой каталог с текущей датой и временем. Затем вы можете прочитать значение во время выполнения. Это немного взломано, но должно работать.

person MikeWyatt    schedule 21.10.2009

Я не уверен, но, возможно, поможет Build Incrementer.

person Bobby    schedule 21.10.2009

Я воспользовался предложением Абдуррахима. Тем не менее, казалось, что это дает странный формат времени, а также добавляет аббревиатуру дня как часть даты сборки; Пример: Вс 24.12.2017 13:21: 05.43. Мне нужна была только дата, поэтому мне пришлось удалить остальное, используя подстроку.

После добавления echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt" в событие предварительной сборки я сделал следующее:

string strBuildDate = YourNamespace.Properties.Resources.BuildDate;
string strTrimBuildDate = strBuildDate.Substring(4).Remove(10);

Хорошая новость в том, что это сработало.

person DemarcPoint    schedule 24.12.2017
comment
Очень простое решение. Мне это нравится. А если формат беспокоит, есть способы получить лучший формат из командной строки. - person Nyerguds; 18.06.2018

Вы можете запустить дополнительный шаг в процессе сборки, который записывает дату в файл, который затем может быть отображен.

На вкладке свойств проекта посмотрите вкладку событий сборки. Есть возможность выполнить команду до или после сборки.

person Guy van den Berg    schedule 21.10.2009

Я только что добавил команду события перед сборкой:

powershell -Command Get-Date -Format 'yyyy-MM-ddTHH:mm:sszzz' > Resources\BuildDateTime.txt

в свойствах проекта, чтобы сгенерировать файл ресурсов, который затем будет легко читать из кода.

person Syr    schedule 22.09.2020

У меня возникли трудности с предлагаемыми решениями в моем проекте, веб-приложении .Net Core 2.1. Я объединил различные предложения сверху и упростил, а также преобразовал дату в требуемый мне формат.

Команда echo:

echo Build %DATE:~-4%/%DATE:~-10,2%/%DATE:~-7,2% %time% > "$(ProjectDir)\BuildDate.txt"

Код:

Logger.Info(File.ReadAllText(@"./BuildDate.txt").Trim());

Вроде работает. Выход:

2021-03-25 18:41:40,877 [1] INFO Config - Build 2021/03/25 18:41:37.58

Ничего очень оригинального, я просто объединил предложения отсюда и другие связанные вопросы и упростил.

person Jonathan Rosenne    schedule 25.03.2021

Если это приложение для Windows, вы можете просто использовать путь к исполняемому файлу приложения: new System.IO.FileInfo (Application.ExecutablePath) .LastWriteTime.ToString ("yyyy.MM.dd")

person John Clark    schedule 19.01.2015
comment
Уже и отвечаю с помощью этого, да и не совсем пуленепробиваемого. - person crashmstr; 19.01.2015

это может быть Assembly execAssembly = Assembly.GetExecutingAssembly(); var creationTime = new FileInfo(execAssembly.Location).CreationTime; // "2019-09-08T14:29:12.2286642-04:00"

person Sasha Bond    schedule 08.09.2019
comment
Разве это не то же самое, что и этот другой ответ? - person Wai Ha Lee; 08.09.2019