Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET 6 DateOnly and TimeOnly mapping support #1715

Closed
arnederuwe opened this issue Oct 7, 2021 · 21 comments · Fixed by #2051
Closed

.NET 6 DateOnly and TimeOnly mapping support #1715

arnederuwe opened this issue Oct 7, 2021 · 21 comments · Fixed by #2051

Comments

@arnederuwe
Copy link

.NET 6 will introduce the DateOnly and TimeOnly structs, these are good mapping candidates for SQL Server's Date and Time types.

There is an open issue in the .NET SqlClient repo here

Am I correct to assume that once SqlClient supports it, Dapper will implicitly support it as well? Or is there some work required in this repo as well in order for this to work?

@roji
Copy link

roji commented Oct 18, 2021

I gave this a try with an Npgsql 6.0 RC, which does already support DateOnly/TimeOnly, and it seems this doesn't work:

await conn.ExecuteScalarAsync("SELECT @Foo", new { Foo = new DateOnly(2020, 1, 1) })

Throws:

Unhandled exception. System.NotSupportedException: The member Foo of type System.DateOnly cannot be used as a parameter value
   at Dapper.SqlMapper.LookupDbType(Type type, String name, Boolean demand, ITypeHandler& handler) in /_/Dapper/SqlMapper.cs:line 417
   at Dapper.SqlMapper.CreateParamInfoGenerator(Identity identity, Boolean checkForDuplicates, Boolean removeUnused, IList`1 literals) in /_/Dapper/SqlMapper.cs:line 2504
   at Dapper.SqlMapper.GetCacheInfo(Identity identity, Object exampleParameters, Boolean addToCache) in /_/Dapper/SqlMapper.cs:line 1727
   at Dapper.SqlMapper.ExecuteScalarImplAsync[T](IDbConnection cnn, CommandDefinition command) in /_/Dapper/SqlMapper.Async.cs:line 1200
   at Program.<Main>$(String[] args) in /home/roji/projects/test/Program.cs:line 12
   at Program.<Main>$(String[] args) in /home/roji/projects/test/Program.cs:line 12
   at Program.<Main>(String[] args)

Somewhat related to #1716... I know nothing about Dapper internals, but ideally it would be possible to use any arbitrary .NET type as a parameter, and Dapper would simply pass that along to the DbParameter.Value, without anything else (that would obviate needing any special type handlers or something). But there may be some good reason why things don't work this way currently.

@FatTigerWang
Copy link

FatTigerWang commented Oct 19, 2021

I tracked the source code and found that the TimeOnly and DateOnly types are not supported here. I am not sure if adding these two types to the collection and specifying them as DbType.Date and DbType.Time will work. I will try if it works.

Dapper does not support .NET 6, so DateOnly and TimeOnly types cannot be used

Reference: System.Data.DbType

@kevingates
Copy link

Any timeline when this may be fixed?

@mgravell
Copy link
Member

mgravell commented Dec 9, 2021

@kevingates see also #1728; there is an open branch, but we need to first be sure how this is intended to interact with the various providers

@szalapski
Copy link

Very much would like to see this. It is weird that SQL Server's DATE type should map to anything but DateOnly in C#.

@h181422
Copy link

h181422 commented Jun 3, 2022

For those who are looking for a workaround: make your own type handler.

Add the following to your configuration:

SqlMapper.AddTypeHandler(new SqlTimeOnlyTypeHandler());

public class SqlTimeOnlyTypeHandler : SqlMapper.TypeHandler<TimeOnly>
{
    public override void SetValue(IDbDataParameter parameter, TimeOnly time)
    {
        parameter.Value = time.ToString();
    }

    public override TimeOnly Parse(object value)
    {
        return TimeOnly.FromTimeSpan((TimeSpan)value);
    }
}

@danielearwicker
Copy link

And similarly for DateOnly:

public class DapperSqlDateOnlyTypeHandler : SqlMapper.TypeHandler<DateOnly>
{
    public override void SetValue(IDbDataParameter parameter, DateOnly date)
        => parameter.Value = date.ToDateTime(new TimeOnly(0, 0));
    
    public override DateOnly Parse(object value)
        => DateOnly.FromDateTime((DateTime)value);
}

@K0rhak
Copy link

K0rhak commented Jun 17, 2022

I had problems with database conversion so I had to specify the type. Like this

public override void SetValue(IDbDataParameter parameter, DateOnly date)
{
    parameter.DbType = DbType.DateTime;
    parameter.Value = date.ToDateTime(new TimeOnly(0, 0));
}

@PedroC88
Copy link

PedroC88 commented Jul 8, 2022

How exactly is the TypeHandler used? I have the following line in the constructor of my DB class

SqlMapper.AddTypeHandler(new DateOnlyTypeHandler());
SqlMapper.AddTypeHandler(new TimeOnlyTypeHandler());

and the following TypeHandlers

public class DateOnlyTypeHandler : SqlMapper.TypeHandler<DateOnly>
{
    public override DateOnly Parse(object value) => DateOnly.FromDateTime((DateTime)value);

    public override void SetValue(IDbDataParameter parameter, DateOnly value)
    {
        parameter.DbType = DbType.Date;
        parameter.Value = value;
    }
}

public class TimeOnlyTypeHandler : SqlMapper.TypeHandler<TimeOnly>
{
    public override TimeOnly Parse(object value) => TimeOnly.FromDateTime((DateTime)value);

    public override void SetValue(IDbDataParameter parameter, TimeOnly value)
    {
        parameter.DbType = DbType.Time;
        parameter.Value = value;
    }
}

But I'm still getting a an exception that the Date field in my object is NULL and the breakpoints on the convertion methods aren't being hit.

@h181422
Copy link

h181422 commented Jul 8, 2022

SqlMapper.AddTypeHandler(new DateOnlyTypeHandler());
SqlMapper.AddTypeHandler(new TimeOnlyTypeHandler());

I've got them in my container config, where they are registered at startup.

@PedroC88
Copy link

That makes more sense indeed. And I also realized that z.dapper.pluss doesn't use the TypeHandlers for bulk inserts, which is why the breakpoints weren't hitting the methods in the first place.

@ScottRFrost
Copy link

Found this issue in a search and was able to get the type handlers working (The SqlMapper.AddTypeHandler lines go in Startup.cs \ ConfigureServices if you're working on an API by they way, that took me a few minutes to figure out).

However, to make them work in converting either a MS SQL DateTime OR a MS SQL Time to TimeOnly, I needed this modification (MS SQL Date to DateOnly seemed to work without change):

    public class DateOnlyTypeHandler : SqlMapper.TypeHandler<DateOnly> // Dapper handler for DateOnly
    {
        public override DateOnly Parse(object value) => DateOnly.FromDateTime((DateTime)value);

        public override void SetValue(IDbDataParameter parameter, DateOnly value)
        {
            parameter.DbType = DbType.Date;
            parameter.Value = value;
        }
    }

    public class TimeOnlyTypeHandler : SqlMapper.TypeHandler<TimeOnly> // Dapper handler for TimeOnly
    {
        public override TimeOnly Parse(object value)
        {
            if (value.GetType() == typeof(DateTime))
            {
                return TimeOnly.FromDateTime((DateTime)value);
            }
            else if (value.GetType() == typeof(TimeSpan))
            {
                return TimeOnly.FromTimeSpan((TimeSpan)value);
            }
            return default;
        }

        public override void SetValue(IDbDataParameter parameter, TimeOnly value)
        {
            parameter.DbType = DbType.Time;
            parameter.Value = value;
        }
    }

Hopefully these mappings can be added to default dapper one day

@yacine-karim
Copy link

Make sure to declare the DateOnly property is declared as Nullable DateOnly? if the table contains a null values

@AdisonCavani
Copy link

AdisonCavani commented May 19, 2023

SetValue function is called and it works, however Parse is never called. It's always returning NULL

@nhustak
Copy link

nhustak commented Sep 17, 2023

Is there still no native support for DateOnly & TimeOnly?

@mgravell
Copy link
Member

There are some complications on the read side that make it a much bigger change than you would think. I have some ideas,though.

@KeithHenry
Copy link

To make this work with record classes I've been using an alternative constructor, which may be useful to others working around this...

With a select like:

select convert(date, Created) 'day', count(*) 'value' 
from...
group by convert(date, Created)

This crashes:

public record WithDateOnly(DateOnly Day, int Value);

Because there is no constructor accepting System.DateTime, System.Int32, even though SQL has output its native date type.

The workaround is to add an alternative constructor:

public record WithDateOnly(DateOnly Day, int Value) {
    public WithDateOnly(DateTime day, int value) : 
        this(DateOnly.FromDateTime(day), value) { }
}

@VictorioBerra
Copy link

Just hit this today. Would love to see this built-in. DateOnly has been in 2 major .NET versions now.

@mgravell
Copy link
Member

mgravell commented Mar 6, 2024

Untested (not at PC), but you could try:

SqlMapper.AddTypeMap(typeof(DateOnly), (DbType)-1, true);
SqlMapper.AddTypeMap(typeof(TimeOnly), (DbType)-1, true);

It is hard for us to configure this automatically because different providers need different configurations to work correctly here.

mgravell added a commit that referenced this issue Mar 7, 2024
mgravell added a commit that referenced this issue Mar 7, 2024
@mkorsukov
Copy link

After dependencies upgrade to latest versions, new .NET types seem to work fine with Dapper.

<ItemGroup>
  <PackageReference Include="Dapper" Version="2.1.37" />
  <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
</ItemGroup>

And custom type handlers SqlMapper.TypeHandler<DateOnly> are not used by Dapper any more.

@nunutu29
Copy link

nunutu29 commented Jul 5, 2024

In some cases (i.e. System.Data.SqlClient) parameter.DbType = DbType.Date is automatically converted to DbType.DateTime (See here).

private class DateOnlyTypeHandler : SqlMapper.TypeHandler<DateOnly>
{
    public override void SetValue(IDbDataParameter parameter, DateOnly value)
    {
        // Sets the parameter's data type to DbType.Date.
        parameter.DbType = DbType.Date;

        // Note:
        // - In System.Data.SqlClient, setting DbType.Date is automatically converted to DbType.DateTime.
        // - In Microsoft.Data.SqlClient, setting remains DbType.Date.

        if (parameter.DbType == DbType.DateTime)
        {
            // If the parameter's data type has been changed to DbType.DateTime,
            // converts the DateOnly value to DateTime, setting the time to zero.
            parameter.Value = value.ToDateTime(new TimeOnly(0, 0, 0, 0));
        }
        else
        {
            // Otherwise, directly assigns the DateOnly value to the parameter.
            parameter.Value = value;
        }
    }

    public override DateOnly Parse(object value)
    {
        // Checks if the value is already of type DateOnly
        if (value is DateOnly dtOnly)
        {
            return dtOnly;
        }
        
        // Checks if the value is of type DateTime and converts it to DateOnly
        if (value is DateTime dt)
        {
            return DateOnly.FromDateTime(dt);
        }

        // If the value is neither DateOnly nor DateTime, throws an exception with an error message
        throw new InvalidOperationException($"Errore durante la conversione DateOnly: {value}" );
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.