I get a Date in an ASP.NET Core Controller like this:
public class MyController:Controller{
public IActionResult Test(DateTime date) {
}
}
Had the same problem. While passing DateTime in request body works fine (because Json converter handles this staff), passing DateTime in query string as a parameter has some culture issues.
I did not like the "change all requests culture" approach, bacause this could have impact on other type's parsing, which is not desirable.
So my choise was to override the default DateTime model binding using IModelBinder: https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding
What I did:
1) Define custom binder (c# 7 syntax for 'out' parameter is used):
public class DateTimeModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
// Try to fetch the value of the argument by name
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
return Task.CompletedTask;
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
var dateStr = valueProviderResult.FirstValue;
// Here you define your custom parsing logic, i.e. using "de-DE" culture
if (!DateTime.TryParse(dateStr, new CultureInfo("de-DE"), DateTimeStyles.None, out DateTime date))
{
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, "DateTime should be in format 'dd.MM.yyyy HH:mm:ss'");
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(date);
return Task.CompletedTask;
}
}
2) Define provider for your binder:
public class DateTimeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(DateTime) ||
context.Metadata.ModelType == typeof(DateTime?))
{
return new DateTimeModelBinder();
}
return null;
}
}
3) And finally, register your provider to be used by ASP.NET Core:
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider());
});
Now your DateTime will be parsed as expected.
DateTime dt = DateTime.ParseExact(dateString, "ddMMyyyy", CultureInfo.InvariantCulture);
dt.ToString("yyyyMMdd");
As per https://stackoverflow.com/a/3477821/2914174
It is better to send your date from front to controller in the ISO format: "yyyy-MM-dd"
https://www.w3schools.com/js/js_date_formats.asp
Any server side with any culture will understand this date format correctly.
So, I'm using sending like this:
const dateStart = new Date();
$.post("localhost:4200/start", { dateStart: dateStart.toISOString() },
function(data) {
console.log("Started!");
});
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Globalization;
using Microsoft.AspNetCore.Localization;
namespace coreweb
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
if (env.IsDevelopment())
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// ... previous configuration not shown
services.AddMvc();
services.Configure<RequestLocalizationOptions>(
opts =>
{
var supportedCultures = new[]
{
new CultureInfo("de-DE"),
};
opts.DefaultRequestCulture = new RequestCulture("de-DE");
// Formatting numbers, dates, etc.
opts.SupportedCultures = supportedCultures;
// UI strings that we have localized.
opts.SupportedUICultures = supportedCultures;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// app.UseApplicationInsightsRequestTelemetry();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
// app.UseApplicationInsightsExceptionTelemetry();
app.UseStaticFiles();
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Consider using a custom TypeConverter
for your datetime (Source):
using System;
using System.ComponentModel;
using System.Globalization;
using System.Drawing;
public class DeDateTimeConverter : TypeConverter {
// Overrides the CanConvertFrom method of TypeConverter.
// The ITypeDescriptorContext interface provides the context for the
// conversion. Typically, this interface is used at design time to
// provide information about the design-time container.
public override bool CanConvertFrom(ITypeDescriptorContext context,
Type sourceType) {
if (sourceType == typeof(string)) {
return true;
}
return base.CanConvertFrom(context, sourceType);
}
// Overrides the ConvertFrom method of TypeConverter.
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value) {
if (value is string) {
if (DateTime.TryParse(((string)value), new CultureInfo("de-DE") /*or use culture*/, DateTimeStyles.None, out DateTime date))
return date;
}
return base.ConvertFrom(context, culture, value);
}
}
and use TypeConverter
attribute on your property:
[TypeConverter(typeof(DeDateTimeConverter))]
public DateTime CustomDateTime { get; set; }
Update
Based on my experience and thanks to this answer and @zdeněk comment, TypeConverter attribute do not work and you should register TypeConverter in Startup.cs
:
TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(DeDateTimeConverter)));
MVC has always used InvariantCulture
for route data and query strings (parameters that go in the URL). The reason behind it is that URLs in localized application must be universal. Otherwise, one url can provide different data depending on the user locale.
You can replace the query and route ValueProviderFactories with your own that respect current culture (or use method="POST"
in forms)
public class CustomValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var query = context.ActionContext.HttpContext.Request.Query;
if (query != null && query.Count > 0)
{
var valueProvider = new QueryStringValueProvider(
BindingSource.Query,
query,
CultureInfo.CurrentCulture);
context.ValueProviders.Add(valueProvider);
}
return Task.CompletedTask;
}
}
services.AddMvc(opts => {
// 2 - Index QueryStringValueProviderFactory
opts.ValueProviderFactories[2] = new CustomValueProviderFactory();
})
P.S. It is reasonable behavior, but I don't understand why documentation don't cover this very important thing.