diff --git a/net/DevExtreme.AspNet.Data.Tests.Common/RemoteGroupingStressHelper.cs b/net/DevExtreme.AspNet.Data.Tests.Common/RemoteGroupingStressHelper.cs index 51597bd9..a772ceaa 100644 --- a/net/DevExtreme.AspNet.Data.Tests.Common/RemoteGroupingStressHelper.cs +++ b/net/DevExtreme.AspNet.Data.Tests.Common/RemoteGroupingStressHelper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Xunit; namespace DevExtreme.AspNet.Data.Tests { @@ -11,12 +10,18 @@ public static class RemoteGroupingStressHelper { const string PROP_NULL_NUM = nameof(IEntity.NullNum); const string PROP_DATE = nameof(IEntity.Date); const string PROP_NULL_DATE = nameof(IEntity.NullDate); +#if EFCORE8 || EFCORE9 + const string PROP_DATE_ONLY = nameof(IEntity.DateO); +#endif public interface IEntity { int Num { get; } int? NullNum { get; } DateTime Date { get; } DateTime? NullDate { get; } +#if EFCORE8 || EFCORE9 + DateOnly DateO { get; set; } +#endif } public static void Run(IQueryable data) where T : IEntity { @@ -36,7 +41,10 @@ static GroupingInfo[] BuildGroupParams() { new GroupingInfo { Selector = PROP_NUM }, new GroupingInfo { Selector = PROP_NULL_NUM }, new GroupingInfo { Selector = PROP_DATE }, - new GroupingInfo { Selector = PROP_NULL_DATE } + new GroupingInfo { Selector = PROP_NULL_DATE }, +#if EFCORE8 || EFCORE9 + new GroupingInfo { Selector = PROP_DATE_ONLY }, +#endif }; foreach(var interval in Enumerable.Range(1, 3).Select(i => (100 * i).ToString())) { diff --git a/net/DevExtreme.AspNet.Data.Tests.EFCore/RemoteGroupingStress.cs b/net/DevExtreme.AspNet.Data.Tests.EFCore/RemoteGroupingStress.cs index 8608ffe7..3ec402e8 100644 --- a/net/DevExtreme.AspNet.Data.Tests.EFCore/RemoteGroupingStress.cs +++ b/net/DevExtreme.AspNet.Data.Tests.EFCore/RemoteGroupingStress.cs @@ -15,6 +15,9 @@ public class DataItem : RemoteGroupingStressHelper.IEntity { public int? NullNum { get; set; } public DateTime Date { get; set; } public DateTime? NullDate { get; set; } +#if EFCORE8 || EFCORE9 + public DateOnly DateO { get; set; } +#endif } [Fact] diff --git a/net/DevExtreme.AspNet.Data.Tests.NH/RemoteGroupingStress.cs b/net/DevExtreme.AspNet.Data.Tests.NH/RemoteGroupingStress.cs index 0d28e1e7..6f0dc7aa 100644 --- a/net/DevExtreme.AspNet.Data.Tests.NH/RemoteGroupingStress.cs +++ b/net/DevExtreme.AspNet.Data.Tests.NH/RemoteGroupingStress.cs @@ -13,6 +13,10 @@ public class DataItem : RemoteGroupingStressHelper.IEntity { public virtual int? NullNum { get; set; } public virtual DateTime Date { get; set; } public virtual DateTime? NullDate { get; set; } +//#if EFCORE8 || EFCORE9 + // dummy interface implementation + public virtual DateOnly DateO { get; set; } +//#endif } public class DataItemMap : ClassMap { @@ -23,10 +27,11 @@ public DataItemMap() { Map(i => i.NullNum); Map(i => i.Date); Map(i => i.NullDate); + //Map(i => i.DateO); //used by all fixtures, requires nh feature support (see skip) } } - [Fact] + [Fact(Skip = "Skip until https://github.com/nhibernate/nhibernate-core/issues/2912 is implemented?")] public async Task Scenario() { await SessionFactoryHelper.ExecAsync(session => { session.Save(new DataItem()); diff --git a/net/DevExtreme.AspNet.Data.Tests.Xpo/DevExtreme.AspNet.Data.Tests.Xpo.csproj b/net/DevExtreme.AspNet.Data.Tests.Xpo/DevExtreme.AspNet.Data.Tests.Xpo.csproj index 904fec4e..c273d591 100644 --- a/net/DevExtreme.AspNet.Data.Tests.Xpo/DevExtreme.AspNet.Data.Tests.Xpo.csproj +++ b/net/DevExtreme.AspNet.Data.Tests.Xpo/DevExtreme.AspNet.Data.Tests.Xpo.csproj @@ -1,11 +1,11 @@ - + net6.0 - + diff --git a/net/DevExtreme.AspNet.Data.Tests.Xpo/RemoteGroupingStress.cs b/net/DevExtreme.AspNet.Data.Tests.Xpo/RemoteGroupingStress.cs index a7e6d602..e5473f63 100644 --- a/net/DevExtreme.AspNet.Data.Tests.Xpo/RemoteGroupingStress.cs +++ b/net/DevExtreme.AspNet.Data.Tests.Xpo/RemoteGroupingStress.cs @@ -14,6 +14,9 @@ public class DataItem : XPLiteObject, RemoteGroupingStressHelper.IEntity { int? _nullNum; DateTime _date; DateTime? _nullDate; +//#if EFCORE8 || EFCORE9 + DateOnly _dateO; +//#endif public DataItem(Session session) : base(session) { @@ -45,9 +48,15 @@ public DateTime? NullDate { set { SetPropertyValue(nameof(NullDate), ref _nullDate, value); } } +//#if EFCORE8 || EFCORE9 + public DateOnly DateO { + get { return _dateO; } + set { SetPropertyValue(nameof(DateO), ref _dateO, value); } + } +//#endif } - [Fact] + [Fact(Skip = "Skip until proper DevExpress.Xpo dll / nupkg with Date Time Only support?")] public async Task Scenario() { await UnitOfWorkHelper.ExecAsync(uow => { new DataItem(uow); diff --git a/net/DevExtreme.AspNet.Data/GroupHelper.cs b/net/DevExtreme.AspNet.Data/GroupHelper.cs index d838e56d..c037d401 100644 --- a/net/DevExtreme.AspNet.Data/GroupHelper.cs +++ b/net/DevExtreme.AspNet.Data/GroupHelper.cs @@ -1,11 +1,9 @@ using DevExtreme.AspNet.Data.Helpers; using DevExtreme.AspNet.Data.ResponseModel; + using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; namespace DevExtreme.AspNet.Data { @@ -96,6 +94,11 @@ static DateTime ToDateTime(object value) { if(value is DateTimeOffset offset) return offset.DateTime; +#if NET6_0_OR_GREATER + if(value is DateOnly date) + return date.ToDateTime(TimeOnly.MinValue); +#endif + return Convert.ToDateTime(value); } } diff --git a/net/DevExtreme.AspNet.Data/Utils.cs b/net/DevExtreme.AspNet.Data/Utils.cs index c24c70d0..675253e2 100644 --- a/net/DevExtreme.AspNet.Data/Utils.cs +++ b/net/DevExtreme.AspNet.Data/Utils.cs @@ -48,6 +48,13 @@ public static object ConvertClientValue(object value, Type type) { if(type == typeof(DateTimeOffset) && value is DateTime date) return new DateTimeOffset(date); +#if NET6_0_OR_GREATER + if(type == typeof(DateOnly) && value is String) + return DateOnly.Parse((string)value, CultureInfo.InvariantCulture); + if(type == typeof(TimeOnly) && value is String) + return TimeOnly.Parse((string)value, CultureInfo.InvariantCulture); +#endif + var converter = TypeDescriptor.GetConverter(type); if(converter != null && converter.CanConvertFrom(value.GetType())) return converter.ConvertFrom(null, CultureInfo.InvariantCulture, value); diff --git a/net/Sample/Controllers/NorthwindController.cs b/net/Sample/Controllers/NorthwindController.cs index b4453804..c8a26699 100644 --- a/net/Sample/Controllers/NorthwindController.cs +++ b/net/Sample/Controllers/NorthwindController.cs @@ -1,12 +1,12 @@ using DevExtreme.AspNet.Data; -using Sample.Models; - using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using Sample.Models; + namespace Sample.Controllers { [Route("nwind")] @@ -23,6 +23,10 @@ public async Task Orders(DataSourceLoadOptions loadOptions) { o.OrderId, o.CustomerId, o.OrderDate, + //---------------------------------------- + o.OrderDateOnly, + o.OrderTimeOnly, + //---------------------------------------- o.Freight, o.ShipCountry, o.ShipRegion, diff --git a/net/Sample/Converters.cs b/net/Sample/Converters.cs new file mode 100644 index 00000000..4f79413d --- /dev/null +++ b/net/Sample/Converters.cs @@ -0,0 +1,36 @@ +#if NET6_0 +using System.Text.Json; +using System.Text.Json.Serialization; +#endif + +#if NET6_0 || NET7_0 +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +#endif + +#if NET6_0 +// TODO: Remove after migrate to net7+ +// https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-7/ +// https://stackoverflow.com/questions/74246482/ +public sealed class Net6DateOnlyJsonConverter : JsonConverter { + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => DateOnly.FromDateTime(reader.GetDateTime()); + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString("O")); +} +public sealed class Net6TimeOnlyJsonConverter : JsonConverter { + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => TimeOnly.Parse(reader.GetString()); + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString("HH:mm:ss.fff")); +} +#endif + +#if NET6_0 || NET7_0 +public sealed class NetLess8DateOnlyValueConverter : ValueConverter { + public NetLess8DateOnlyValueConverter() : base( + dateOnly => dateOnly.ToDateTime(TimeOnly.MinValue), + dateTime => DateOnly.FromDateTime(dateTime)) { } +} + +public sealed class NetLess8TimeOnlyValueConverter : ValueConverter { + public NetLess8TimeOnlyValueConverter() : base( + timeOnly => timeOnly.ToTimeSpan(), + timeSpan => TimeOnly.FromTimeSpan(timeSpan)) { } +} +#endif diff --git a/net/Sample/Models/NorthwindContext.cs b/net/Sample/Models/NorthwindContext.cs index 49742f8a..fdc7d688 100644 --- a/net/Sample/Models/NorthwindContext.cs +++ b/net/Sample/Models/NorthwindContext.cs @@ -125,5 +125,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { .HasConstraintName("FK_Products_Categories"); }); } + +#if NET6_0 || NET7_0 + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { + base.ConfigureConventions(configurationBuilder); + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + } +#endif + } } diff --git a/net/Sample/Models/Order.cs b/net/Sample/Models/Order.cs index 5ce00899..c5d5882c 100644 --- a/net/Sample/Models/Order.cs +++ b/net/Sample/Models/Order.cs @@ -20,9 +20,21 @@ public Order() { [Column("EmployeeID")] public int? EmployeeId { get; set; } + //---------------------------------------- + [Column(TypeName = "datetime")] public DateTime? OrderDate { get; set; } + // [NotMapped] + // [Column(TypeName = "date")] + // public DateOnly? OrderDateOnly => DateOnly.FromDateTime(OrderDate.Value); + + public DateOnly? OrderDateOnly { get; set; } + + public TimeOnly? OrderTimeOnly { get; set; } + + //---------------------------------------- + [Column(TypeName = "datetime")] public DateTime? RequiredDate { get; set; } diff --git a/net/Sample/Program.cs b/net/Sample/Program.cs index 83651ea8..5aa338f1 100644 --- a/net/Sample/Program.cs +++ b/net/Sample/Program.cs @@ -7,7 +7,14 @@ public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services .AddControllersWithViews() - .AddRazorRuntimeCompilation(); + //.AddRazorRuntimeCompilation() +#if NET6_0 + .AddJsonOptions(options => { + options.JsonSerializerOptions.Converters.Add(new Net6DateOnlyJsonConverter()); + options.JsonSerializerOptions.Converters.Add(new Net6TimeOnlyJsonConverter()); + }) +#endif + ; builder.Services .AddLogging() .AddEntityFrameworkSqlServer() diff --git a/net/Sample/SQL.txt b/net/Sample/SQL.txt new file mode 100644 index 00000000..813394dc --- /dev/null +++ b/net/Sample/SQL.txt @@ -0,0 +1,18 @@ +USE [Northwind] +GO +ALTER TABLE [Orders] +ADD [OrderDateOnly] date NULL +GO +UPDATE [Orders] SET [OrderDateOnly] = CONVERT(date, [OrderDate]) +GO +ALTER TABLE [Orders] +ADD [OrderTimeOnly] time NULL +GO +UPDATE [Orders] SET [OrderTimeOnly] = CONVERT(time, DATEADD(hour, ([OrderID] + 1), [OrderDate])) + +--- + +dotnet add package Microsoft.EntityFrameworkCore.Tools +dotnet add package Microsoft.EntityFrameworkCore.SqlServer + +Scaffold-DbContext "Data Source=(localdb)\MSSQLLocalDB; Database=Northwind; Integrated Security=True; MultipleActiveResultSets=True; App=EntityFramework" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models diff --git a/net/Sample/Sample.csproj b/net/Sample/Sample.csproj index 1cb12f9a..77c36022 100644 --- a/net/Sample/Sample.csproj +++ b/net/Sample/Sample.csproj @@ -1,18 +1,43 @@  - net6.0 + net6.0;net7.0;net8.0 enable - - - + + + NET6.0 + + + + + + + + + NET7.0 + + + + + + + + + NET8.0 + + + + + + + diff --git a/net/Sample/Views/Home/Index.cshtml b/net/Sample/Views/Home/Index.cshtml index fdca3920..f455e4e6 100644 --- a/net/Sample/Views/Home/Index.cshtml +++ b/net/Sample/Views/Home/Index.cshtml @@ -39,6 +39,20 @@ groupInterval: "quarter" } }, + { + dataField: "orderDateOnly", + dataType: "date", + headerFilter: { + groupInterval: "quarter" + } + }, + { + dataField: "orderTimeOnly", + dataType: "time", + headerFilter: { + //groupInterval: "quarter" // N|A + } + }, { dataField: "freight", headerFilter: { @@ -61,8 +75,13 @@ ], groupPanel: { visible: true }, + searchPanel: { visible: true }, filterRow: { visible: true }, - headerFilter: { visible: true }, + filterPanel: { visible: true }, + headerFilter: { + allowSearch: true, + visible: true + }, editing: { allowUpdating: true, allowAdding: true, @@ -74,7 +93,10 @@ }, summary: { totalItems: [ - { column: "freight", summaryType: "sum" } + { column: "freight", summaryType: "sum" }, + { column: "orderDate", summaryType: "count" }, + { column: "orderDateOnly", summaryType: "count" }, + //{ column: "orderTimeOnly", summaryType: "count" } ], groupItems: [ { column: "freight", summaryType: "sum" },