Ok, I have figured it out myself by downloading and investigating the EF source code. A few classes need to be overriden. This works in EF Core 2.1.2 which I am using so no guarantees for older or newer versions (as the API states that these classes may be changed) but hopefully only small changes if there are issues.
So the following will enable a DateTime property (representing just the date) in the POCO to be useable with the actual SQL data being a string. It will be simple to derive additional classes for Times and Booleans.
Need a class to convert between string and date:
public class StringDateConverter : ValueConverter<DateTime?, string>
{
// these can be overridden
public static string StringDateStorageType = "char(8)";
public static string StringDateStorageFormat = "yyyyMMdd";
public static string StringDateEmptyValue = "00000000";
protected static readonly ConverterMappingHints _defaultHints
= new ConverterMappingHints(size: 48);
public StringDateConverter()
: base(ToString(), ToDateTime(), _defaultHints)
{
}
protected new static Expression<Func<DateTime?, string>> ToString()
=> v => DateToString(v);
protected static Expression<Func<string, DateTime?>> ToDateTime()
=> v => StringToDate(v);
private static string DateToString(DateTime? date)
{
if (date.HasValue)
return date.Value.ToString(StringDateStorageFormat);
return StringDateEmptyValue;
}
private static DateTime? StringToDate(string date)
{
if (!string.IsNullOrWhiteSpace(date)
&& !(date == StringDateEmptyValue)
&& DateTime.TryParseExact(date, StringDateStorageFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime result))
return result;
return null;
}
}
This class inherits the EF SqlServerDateTimeTypeMapping and makes use of the above converter.
public class SqlServerDateTypeMapping : Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerDateTimeTypeMapping
{
public SqlServerDateTypeMapping()
: this(StringDateConverter.StringDateStorageType, System.Data.DbType.String)
{
}
public SqlServerDateTypeMapping(string storeType, DbType? dbType = null)
: base(storeType, dbType)
{
}
protected SqlServerDateTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}
public override DbType? DbType => System.Data.DbType.String;
protected override string SqlLiteralFormatString
=> StoreType == StringDateConverter.StringDateStorageType
? "'" + StringDateConverter.StringDateStorageFormat + "'"
: base.SqlLiteralFormatString;
public override ValueConverter Converter => new StringDateConverter();
// ensure cloning returns an instance of this class
public override RelationalTypeMapping Clone(in RelationalTypeMappingInfo mappingInfo)
{
return new SqlServerDateTypeMapping();
}
public override RelationalTypeMapping Clone(string storeType, int? size)
{
return new SqlServerDateTypeMapping();
}
public override CoreTypeMapping Clone(ValueConverter converter)
{
return new SqlServerDateTypeMapping();
}
}
Then need a class to override the type mapping service:
public class SqlServerTypeMappingSource : Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerTypeMappingSource
{
public SqlServerTypeMappingSource(TypeMappingSourceDependencies dependencies, RelationalTypeMappingSourceDependencies relationalDependencies) : base(dependencies, relationalDependencies)
{
}
protected override RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo)
{
if (mappingInfo.ClrType == typeof(DateTime) && mappingInfo.StoreTypeName == StringDateConverter.StringDateStorageType)
return new SqlServerDateTypeMapping();
return base.FindMapping(mappingInfo);
}
}
The EF default mapping service can be replaced in the OnConfiguring method of the DbContext:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.ReplaceService<IRelationalTypeMappingSource, CommonComponents.Data.SqlServerTypeMappingSource>();
optionsBuilder.UseSqlServer(Data.Configuration.ConnectionString);
}
Now specifying the property in the POCO looks like this:
[Column(Order = 10, TypeName = "char(8)")]
public DateTime? SomeDate { get; set; }