Correctly convert DateTime property with Dapper on SQLite - datetime

I'm using Dapper to insert and get objects to/from SQLite: one object have a property of type DateTime (and DateTimeOffset) that I have to persist and retrieve with milliseconds precision. I can't find a way to correctly retrieve the value because Dapper fail with:
System.FormatException : String was not recognized as a valid DateTime.
in System.DateTimeParse.ParseExactMultiple(String s, String[] formats, DateTimeFormatInfo dtfi, DateTimeStyles style)
in System.DateTime.ParseExact(String s, String[] formats, IFormatProvider provider, DateTimeStyles style)
in System.Data.SQLite.SQLiteConvert.ToDateTime(String dateText, SQLiteDateFormats format, DateTimeKind kind, String formatString)
in System.Data.SQLite.SQLite3.GetDateTime(SQLiteStatement stmt, Int32 index)
in System.Data.SQLite.SQLite3.GetValue(SQLiteStatement stmt, SQLiteConnectionFlags flags, Int32 index, SQLiteType typ)
in System.Data.SQLite.SQLiteDataReader.GetValue(Int32 i)
in System.Data.SQLite.SQLiteDataReader.GetValues(Object[] values)
in Dapper.SqlMapper.<>c__DisplayClass5d.<GetDapperRowDeserializer>b__5c(IDataReader r) in SqlMapper.cs: line 2587
in Dapper.SqlMapper.<QueryImpl>d__11`1.MoveNext() in SqlMapper.cs: line 1572
in System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
in System.Linq.Enumerable.ToList(IEnumerable`1 source)
in Dapper.SqlMapper.Query(IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Boolean buffered, Nullable`1 commandTimeout, Nullable`1 commandType) in SqlMapper.cs: line 1443
in Dapper.SqlMapper.Query(IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Boolean buffered, Nullable`1 commandTimeout, Nullable`1 commandType) in SqlMapper.cs: line 1382
What do I have to try? Column is of type DATETIME.
Do I have to create a custom TypeHandler and convert DateTime to and from a SQLite string in format "o"?
Dapper version 1.38

I know it's old, but I have found the solution.
After a lot of digging and analyzing Dapper code I came up with this (notice that this is 2019 year):
First you will have to create date time handler:
public class DateTimeHandler : SqlMapper.TypeHandler<DateTimeOffset>
{
private readonly TimeZoneInfo databaseTimeZone = TimeZoneInfo.Local;
public static readonly DateTimeHandler Default = new DateTimeHandler();
public DateTimeHandler()
{
}
public override DateTimeOffset Parse(object value)
{
DateTime storedDateTime;
if (value == null)
storedDateTime = DateTime.MinValue;
else
storedDateTime = (DateTime)value;
if (storedDateTime.ToUniversalTime() <= DateTimeOffset.MinValue.UtcDateTime)
return DateTimeOffset.MinValue;
else
return new DateTimeOffset(storedDateTime, databaseTimeZone.BaseUtcOffset);
}
public override void SetValue(IDbDataParameter parameter, DateTimeOffset value)
{
DateTime paramVal = value.ToOffset(this.databaseTimeZone.BaseUtcOffset).DateTime;
parameter.Value = paramVal;
}
}
Now, notice that Dapper translates .Net's type DateTimeOffset to dbType - DateTimeOffset. You need to remove this mapping and add your own like this:
SqlMapper.RemoveTypeMap(typeof(DateTimeOffset));
SqlMapper.AddTypeHandler(DateTimeHandler.Default);
That's all. Now everytime Dapper will see DateTimeOffset property in your model, it will run your DateTimeHandler to manage this.

I have found that custom TypeHandler for base types can't be used because of default typeMap that is choosen before looking for TypeHandler.
I have opened an issue dapper-dot-net but in the mean time I have solved replacing via reflection the default typeMap with a new one like the previous minus the four key DateTime, DateTime?, DateTimeOffset, DateTimeOffset?

I've made a slight modification to Adam Jachocki's solution as it didn't work for me. I am storing a date as TEXT in Sqlite and Dapper was giving me a string instead of a DateTime as the object value to parse. Apparently, Sqlite stores datetime values using three different data types: INTEGER (unix epoch), TEXT (ISO 8601 YYYY-MM-DD HH:MM:SS.SSS), and REAL ("number of days since noon in Greenwich on November 24, 4741 B.C."). That last one is really out there, so it isn't supported in the code below.
See the sqlite docs and this page for more info.
Below is my implementation of the DateTimeOffset TypeHandler. The rest of Adam's solution remains the same.
internal class DateTimeOffsetHandler : SqlMapper.TypeHandler<DateTimeOffset>
{
private static readonly TimeZoneInfo databaseTimeZone = TimeZoneInfo.Local;
private static readonly DateTime unixOrigin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
public static DateTimeOffsetHandler Default { get; } = new DateTimeOffsetHandler();
public DateTimeOffsetHandler() {}
public override DateTimeOffset Parse(object value)
{
if (!TryGetDateTime(value, out DateTime storedDateValue))
{
throw new InvalidOperationException($"Unable to parse value {value} as DateTimeOffset");
}
if (storedDateValue.ToUniversalTime() <= DateTimeOffset.MinValue.UtcDateTime)
{
return DateTimeOffset.MinValue;
}
else
{
return new DateTimeOffset(storedDateValue, databaseTimeZone.BaseUtcOffset);
}
}
public override void SetValue(IDbDataParameter parameter, DateTimeOffset value)
{
DateTime paramVal = value.ToOffset(databaseTimeZone.BaseUtcOffset).DateTime;
parameter.Value = paramVal;
}
private bool TryGetDateTime(object value, out DateTime dateTimeValue)
{
dateTimeValue = default;
if (value is DateTime d)
{
dateTimeValue = d;
return true;
}
if (value is string v)
{
dateTimeValue = DateTime.Parse(v);
return true;
}
if (long.TryParse(value?.ToString() ?? string.Empty, out long l))
{
dateTimeValue = unixOrigin.AddSeconds(l);
return true;
}
if (float.TryParse(value?.ToString() ?? string.Empty, out float f))
{
throw new InvalidOperationException("Unsupported Sqlite datetime type, REAL.");
}
return false;
}
}

Related

Compare Dates using LINQ

I have a table, which has columns ItemName, purchaseDate and expiryDate. Essentially what i'm trying to do is write a LINQ query that counts and displays all items that are within 45days or less from their expiry date, comparing them using their purchase date.
So far this is what i've been able to do:
public string stringExpiry { get; set; }
public int intExpiry { get; set; }
intExpiry= _context.GetMyItems.Where(p => p.ExpiryDate <= p.PurchaseDate.AddDays(-45)).Count();
stringExpiry= _context.GetMyItems.Where(p=> p.ExpiryDate<=p.PurchaseDate.AddDays(-45)).ToList();
Let's make this a little bit more generic, so you can use it as any other LINQ method. After that we can specialize in a method for your class
LINQ is defined using extension methods. If you are not familiar how to create an extension method, read Extension Methods Demystified
For the examples I use the following class. Any class with two DateTime properties will do
class PurchasedProduct
{
public string Name {get; set;}
public DateTime PurchaseDate {get; set;}
public DateTime ExpiryDate {get; set;}
}
Our goal is a method that does the following:
TimeSpan maxTime = TimeSpan.FromDays(45);
IEnumerable<PurchardProduct> purchasedProducts = ...
IEnumerable<PurchasedProducts> almostExpiredProducts =
purchasedProducts.WhereAlmostExpired(maxTime);
Here we go!
First a method that says whether a class with two DataTimes is near expiry date:
public static bool IsWithinTimeSpan<T>(this T source,
Func<T, DateTime> startDateSelector,
Func<T, DateTime> endDateSelector,
TimeSpan maxTime)
{
return startDateSelector(source) - endDateSelector(source) < maxTime;
}
In words: take your source. Use the startDateSelector to extract the startDate; Use the endDateSelector to extract the endDate. Subtract these two DateTimes, and return true if the result is less than maxTime;
Usage:
PurchasedProduct purchasedProduct = new PurchasedProduct {...};
bool productNearExpiryDate = product.IsWithinTimeSpan(maxTime);
Well, if we can do this with one T, we can do this with a sequence of T:
public static IEnumerable<T> WhereWithinTimeSpan<T>(
this IEnumerable<T> source,
Func<T, DateTime> startDateSelector,
Func<T, DateTime> expiryDateSelector,
TimeSpan maxTime)
{
return source.Where(t => t.IsWithinTimeSpan(
startDateSelector, expiryDateSelector, maxTime));
}
Well that was easy, only one line of code!
This looks very much like our goal, only one more function to go to make it like the method you want:
public static IEnumerable<PurchasedProduct> WhereAlmostExpired(
this IEnumerable<PurchasedProduct> purchasedProducts,
TimeSpan maxTime)
{
return purchasedProducts.WhereWithinTimeSpan(
product => product.PurchaseDate,
product => product.ExpiryDate,
maxTime);
}
Again, only one line of code! Of course, if your sequence of items is not IEnumerable<PurchasedProduct>, but for instance IEnumerable<MyClass>, change this one line of code accordingly.
So now we are able to use it for your problem:
TimeSpan maxTime = TimeSpan.FromDays(45);
IEnumerable<MyClass> myObjects = ...
IEnumerable<MyClass> almostExpiredObjects = myObjects.WhereAlmostExpired(maxTime);
Simple comme bonjour!
One final remark: if you are not certain that startTime is smaller then endTime, don't forget to use absolute value before your comparison

How to set datetime through Groovy setter

i have a long value then i convert the Longvalue in datetime format. I am not sure if the conversion is in the right way, but i am able to get in the right format. Now i am struggling to set the converterted datetime in groovy using setter. #formattedDate can be in date format, i do not know how to save in datetime. I get the error conversion String datetime. Please help.
def time= 1550670822 / 1000;
LocalDateTime dateTime = LocalDateTime.ofEpochSecond(time, 0, ZoneOffset.UTC);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss", Locale.ENGLISH);
String formattedDate = dateTime.format(formatter);
trial.setFinishingTime(formattedDate);
In my class i have for setFinishingTime.
public String getFinishingTime()
{
return getPropertyContainer().getString(FINISHING_TIME, "")
}
public void setFinishingTime(String finishingTime)
{
getPropertyContainer().setString(FINISHING_TIME, finishingTime)
}
This part i am not sure, should it be like this in DateTime format:
public DateTime getFinishingTime()
{
return getPropertyContainer().getDate(FINISHING_TIME, "")
}
public void setFinishingTime(DateTime finishingTime)
{
getPropertyContainer().setDate(FINISHING_TIME, finishingTime)
}
If i change this to DateTime how can i store a DateTime of dd:mm:yyyy pattern which i get from the above code. Please help

Using JsonConverter to keep all datetime values formatted the same

I have a use case in which ALL date/time values must be saved in JSON documents using the following format:
yyyy-MM-ddTHH:mm:ss.fffffff
In other words, all date/time values will be exactly 27 characters in length regardless of the date/time value, for example:
System.DateTime.MinValue = "0001-01-01T00:00:00.0000000"
System.DateTime.MaxValue = "9999-12-31T23:59:59.9999999"
19-May-2018 10:35:12 am = "2018-05-19T10:35:12.0000000"
Unfortunately, Json.Net decides to format date/times in various formats depending on the date/time value. I have tried to use the JsonConverter attribute on date/time properties, ie:
[JsonConverter( typeof( datetime_iso8601_converter ) )]
public System.DateTime created_utc;
My converter class is as follows:
public class datetime_iso8601_converter : Newtonsoft.Json.Converters.IsoDateTimeConverter
{
public datetime_iso8601_converter()
{
// Set format that all DateTime values will use ...
base.DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffffff";
}
}
If I run a test using the debugger, for example:
created_utc = System.DateTime.MinValue;
I can see the following line is executed:
base.DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffffff";
However, the resulting JSON date/time value that gets written is:
"0001-01-01T00:00:00"
SECOND ATTEMPT:
I tried the following converter ...
public class datetime_iso8601_converter : Newtonsoft.Json.Converters.DateTimeConverterBase
{
// The format that all datetime values will use ...
private const String k_fmt_datetime_iso8601 = "yyyy-MM-ddTHH:mm:ss.fffffff";
// Write the given datetime value using the format above ...
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
{
if ( value is System.DateTime )
{
String dt_str;
System.DateTime dt;
dt = ( System.DateTime ) value;
dt_str = dt.ToString( k_fmt_datetime_iso8601 );
writer.WriteRawValue( dt_str );
}
else
{
throw new ArgumentException( "value is not System.DateTime" );
}
}
// Return a datetime value ...
public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
{
if ( reader.TokenType != Newtonsoft.Json.JsonToken.Date )
{
throw new Exception( "Invalid token. Expected Date" );
}
return reader.Value;
}
}
Again using the debugger I can see that dt_str is set to a value of:
"0001-01-01T00:00:00.0000000"
but the value that actually gets written is:
"0001-01-01T00:00:00"
I do not understand why.
HOW can I get all date/time values saved in the format required?
Thanks in advance.
The above issue was present in an environment that was using Azure Functions. The current release of Azure functions 1.0.13 does not seem to support the binding redirects, i.e.:
[JsonConverter( typeof( datetime_iso8601_converter ) )]
However, release 1.22.0 will support JsonSettings and binding redirects (hopefully).
Just ran into the same thing. The solution to is to override Newtonsoft's time format string and have it use fffffff instead of FFFFFFF 🙂
See https://github.com/JamesNK/Newtonsoft.Json/issues/2780#issuecomment-1363704872 for sample code and details.

Custom get and set on attribute

I am trying to setup a product key system in my application, but I want to ensure the attribute has the right size (16 characters).
I tried the following
public class ProductKey
{
public const int ProductKeyLength = 16;
[StringLength(ProductKeyLength, MinimumLength = ProductKeyLength)]
private string _value;
[Required]
[Index(IsUnique = true)]
public string Value {
get
{
var temp = Regex.Replace(this._value, ".{4}", "$0-");
return temp.Trim('-');
}
set { this._value = value.Replace("-", "");}
}
}
I want to enable the user to insert the key with our without hyphen. I get the following error with above code:
Column 'Value' in table 'dbo.ProductKeys' is of a type that is invalid for use as a key column in an index.
As I understood, I need to set a limit to Value so it can be used as a unique key. But, _value has a limit and _value is the actual representation of Value in the database.
Is there a way to set the limit correctly in this case?
Thanks in advance.
You are getting the error because without a StringLength attribute on the Value field, the database column gets created as VARCHAR(MAX) which cannot be used as a key. You need a [StringLength] on the field being used as a key. However, as your getter is returning the key formatted with dashes, you need the key length to be 19:
public class ProductKey
{
public const int ProductKeyLength = 19;
private string _value { get; set; }
[Key]
[Required]
[StringLength(ProductKeyLength, MinimumLength = ProductKeyLength)]
[Index(IsUnique = true)]
public string Value
{
get
{
var temp = Regex.Replace(this._value, ".{4}", "$0-");
return temp.Trim('-');
}
set { this._value = value.Replace("-", ""); }
}
}
You might be better off doing your format conversion in ViewModels and client-side code, as one problem you'll have here is searching - for example...
db.Keys.Add(new ProductKey { Value = "1234-5678-9012-3456" });
db.Keys.Add(new ProductKey { Value = "1234567890123455" });
db.SaveChanges();
Console.WriteLine(db.Keys.Count(k => k.Value.Contains("89"))); // 0
Console.WriteLine(db.Keys.Count(k => k.Value.Contains("8-9"))); // 2

How to work with several fields in DateTime?

public DateTime EnterDeparture()
{
Console.WriteLine("Enter Year:");
return new DateTime().AddYears(int.Parse(Console.ReadLine()));
}
// This will return new DateTime(Without assigned Year) Cause DateTime is value type.
public DateTime EnterDeparture()
{
DateTime EnterDeparture = new DateTime();
Console.WriteLine("Enter Year:");
EnterDeparture.AddYears(int.Parse(Console.ReadLine()));
return EnterDeparture;
}
How to work with several fields in DateTime ? (Year,Days for example) Default constructors aren't suitable.
The DateTime.AddXXX methods return new DateTime instances, the existing struct does not change. Since each method returns a new instance, you can chain the method calls together. At the very least, you want to capture each return value into a variable. For example:
DateTime myDate = DateTime.Today;
DateTime tomorrowAtNoon = myDate.AddDays(1).AddHours(12);
You could have also written it like
DateTime tomorrow = myDate.AddDays(1);
DateTime tomorrowAtNoon = tomorrow.AddHours(12);
Follow?

Resources