Is there a simple way using data annotations or a custom type to use a value stored as a string in SQL as a DateTime in EF?

假装没事ソ 提交于 2019-12-14 03:26:13

问题


I have a database where all of the dates and times are stored as strings [yyyyMMdd] and [hhmmss] respectively.

Is there a data annotation I can add in the POCO class so that it is recognised as the type that it should be?

EG:

[Column(TypeName="varchar(8)", Format="yyyyMMdd"] // Format does not exist!
public DateTime MyDate { get; set; }

Or if not, is there a way to define a custom Type and specify how EF should convert the data between the property value and SQL?

NOTE: I have no desire to use a private and public property pair to convert this client-side. I want basic queries to go to the database which this would prevent.

There is a similar question here which has an answer that I am not looking for: convert-value-when-mapping


回答1:


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; }



回答2:


public class ClassName
{
    private const format = "yyyyMMdd" //for example

    [NotMapped]
    public DateTime Date {
       get {
             DateTime.Parse(Date, format, CultureInfo.InvariantCulture);
           }
       set {
             Date = value.ToString(format);
           }
     }

     [column("Date")]
     public string StringDate { get; set; }
}


来源:https://stackoverflow.com/questions/51948645/is-there-a-simple-way-using-data-annotations-or-a-custom-type-to-use-a-value-sto

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!