34

I get a Date in an ASP.NET Core Controller like this:

public class MyController:Controller{
    public IActionResult Test(DateTime date) {

    }
}

The framework is able to parse the date, but only in English format. When I pass 04.12.2017 as date parameter, I mean the 4th of december 2017. This would get parsed as english date, so my date object gets the value 12th of April 2017. I tried adding german only using this article and also this, but without success.

What needs to be done that ASP.NET Core automatically parse dates in the correct German format?

Update I Tried to set the RequestLocalizationOptions

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

Still not working. I call example.com/Test?date=12.04.2017 and got this in my debugger:

public IActionResult Test(DateTime date) {
    string dateString = date.ToString("d"); // 04.12.2016
    string currentDateString = DateTime.Now.ToString("d"); // 14.01.2016
    return Ok();
}
Joakim Johansson
  • 3,196
  • 1
  • 27
  • 43
Lion
  • 16,606
  • 23
  • 86
  • 148
  • How are you passing in the dates to be parsed, show the format of the URL you do. – Scott Chamberlain Jan 13 '17 at 20:20
  • @ScottChamberlain As I said: An call with Test?date=12.04.2017 parameter results in *04.12.2017* as date object inside the Test function. I expect the unmodified date, so 12.04.2017 instead of 04.12.2017. – Lion Jan 13 '17 at 20:25
  • Pass it as a string, then use `DateTime.ParseExact` to parse it *exactly* the way you want. – Lasse V. Karlsen Jan 13 '17 at 20:26
  • 2
    For a point of clarity (to aid discussion) 04.12.2017 being 4th december *is* english format, or more precisely british english. It being 12th April is US english format. Though the UK would use a / instead of a . as a separator. – Chris Jan 13 '17 at 20:30
  • hi @Lion I pasted Startup.cs for asp.net core project type and set culture info . when I looked at current culture in my controller was set to DE . Please try this if this helps . I tried it showed me dates in DE format . I did not tried with parameter in controller bit om Immediate window . – Yashveer Singh Jan 13 '17 at 20:54
  • you can read more about this on this blog . http://andrewlock.net/adding-localisation-to-an-asp-net-core-application/ and I found git url for this as well https://github.com/aspnet/Entropy/blob/dev/samples/Localization.StarterWeb/Startup.cs – Yashveer Singh Jan 13 '17 at 20:55

10 Answers10

23

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://learn.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.

Igor Fedchenko
  • 408
  • 3
  • 6
  • Worked like a charm. Thanks! – Alex G. May 29 '18 at 17:32
  • [Microsoft Documentation](https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-2.1) suggests not using Custom model binders for converting strings to other types as best practice: _Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option._. Consider using `TypeConverter` – Shahab Oct 15 '18 at 17:39
  • Thanks, worked for me too. Just one note: model binder above is not handling well DateTime? null values. – Velyo Oct 22 '18 at 21:52
15

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)));
Shahab
  • 794
  • 2
  • 12
  • 23
13

I wanted to format the dates in my responses and I did the following in ConfigureServices method:

services.AddMvc()
.AddJsonOptions(options =>
{
    options.SerializerSettings.DateFormatString = "mm/dd/yy, dddd";
});

Hope that helps.

PayamGerami
  • 131
  • 1
  • 4
  • 3
    Thanks. If you only want to use it for a special case it can be specified when converting as well ``var deserialized = JsonConvert.DeserializeObject(myJsonstring, new JsonSerializerSettings { DateFormatString = "dd/MM/yyyy" });`` – Jonny May 16 '18 at 14:13
4

Try setting the culture manually in your web.config

<configuration>
   <system.web>    
      <globalization culture="de-DE" uiCulture="de-DE"/>
   </system.web>
</configuration>

EDIT: Since i just realized this is Core, you can do it this way in StartUp.Configure:

var cultureInfo = new CultureInfo("de-DE");
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
maraaaaaaaa
  • 7,749
  • 2
  • 22
  • 37
  • 5
    The question was about Asp.NET Core – Pawel Jan 13 '17 at 20:22
  • 1
    I already tried setting `CultureInfo.DefaultThreadCurrentCulture` and `CultureInfo.DefaultThreadCurrentUICulture` and at the start of the Test action, they're correctly set to **de-DE**. But ASP.NET core doesn't care, the GET parameter is parsed wrong. – Lion Jan 13 '17 at 20:32
4

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.

Sergei Shvets
  • 1,676
  • 1
  • 14
  • 12
0
              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?}");
                    });
                }
            }
        }
Yashveer Singh
  • 1,914
  • 2
  • 15
  • 23
  • Not working. Is applied to `DateTime.Now` but not the argument which got parsed from the GET parameter. Please see my edit in the question cause its too long for a comment. – Lion Jan 14 '17 at 07:32
  • ohhh . let me check with your edited answer . just to confirm what should be the correct result in this case ? – Yashveer Singh Jan 14 '17 at 07:35
  • 12.04.2017 as input should be parsed as 12th of april 2014 instead 4th of december 2014 like ASP.NET does. – Lion Jan 14 '17 at 07:40
0

If you don't mind using the generic StatusCode method to make this call, you can do something like the following:

internal IActionResult CreateResponse(int code, object content = null)
    {
        Type t = content?.GetType();
        bool textContent = t == typeof(string) || t == typeof(bool);
        //
        JsonSerializerSettings dateFormatSettings = new JsonSerializerSettings
        {

            DateFormatString = myDateFormat
        };

        string bodyContent = content == null || string.IsNullOrWhiteSpace(content + "")
                    ? null
                    : textContent
                        ? content + ""
                        : JsonConvert.SerializeObject(content, dateFormatSettings);

        ObjectResult or = base.StatusCode(code, bodyContent);
        string mediaType = 
                    !textContent
                        ? "application/json"
                        : "text/plain";
        or.ContentTypes.Add(new MediaTypeHeaderValue(mediaType));
        return or;
    }

You can add this to a base class and call it like:

return base.CreateResponse(StatusCodes.Status200OK, new { name = "My Name", age = 23});

It's up to you if you want to create your own Ok, BadRequest, etc...methods, but for me this works and I hope it helps anybody else. You could even default int code = 200, if most of your requests are GETs. This code assumes you either want to respond with a string, boolean, or a custom object, but you can easily handle all primitives by checking Type.GetTypeInfo().IsPrimitive and even doing some checks for decimal, string, DateTime, TimeSpan, DateTimeOffset, or Guid.

pqsk
  • 2,124
  • 3
  • 23
  • 28
0

I had same problem ad almost got mad. I tried everthing with no sucsses. First I found a workaround to solve part of my problem:

Workaround:

string data1 
string horainicio 
string horafim

var ageData = new AgendaData();
var user = await _userManager.GetUserAsync(User);
string usuario = user.Id;
int empresa = user.IdEmpresa;
int Idprospect = Convert.ToInt32(prospect);
int minutos = 0;           
var tipoAgenda = TipoAgenda.Contato;

var provider = CultureInfo.InvariantCulture;
provider = new CultureInfo("en-US");            
string formato = "dd/MM/yyyy HH:mm";

var dataInicio = DateTime.ParseExact(data1 + " " + horainicio, formato, provider);
var dataFim = DateTime.ParseExact(data1 + " " + horafim, formato, provider);           
var dataAlerta = dataInicio.AddMinutes(-minutos);

But, this way i aways have to set invariantculture to all my datetime. I found the solution setting my culture at the configure on startup.cs.

Set Culture on startup.cs

 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, CRMContext context)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            //Fixar Cultura para en-US
            RequestLocalizationOptions localizationOptions = new RequestLocalizationOptions
            {
                SupportedCultures = new List<CultureInfo> { new CultureInfo("en-US") },
                SupportedUICultures = new List<CultureInfo> { new CultureInfo("en-US") },
                DefaultRequestCulture = new RequestCulture("en-US")
            };

            app.UseRequestLocalization(localizationOptions);      
            app.UseStaticFiles();
            app.UseIdentity();

            // Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            context.Database.EnsureCreated();
        }

Hope this help you.

Rogerio Azevedo
  • 786
  • 2
  • 13
  • 32
-1

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!");
    });
Rustem Bayetov
  • 127
  • 1
  • 2
  • 5
-4
DateTime dt = DateTime.ParseExact(dateString, "ddMMyyyy", CultureInfo.InvariantCulture);
dt.ToString("yyyyMMdd");

As per https://stackoverflow.com/a/3477821/2914174

Community
  • 1
  • 1
Ryan
  • 25
  • 10
  • 3
    I know this. But doesn't help here cause ASP.NET Core itself parse the GET parameter to an DateTime object. I don't do the parsing. It would be a workaround to use a string instead of DateTime as date argument and then parse it like you suggest. But I want to avoid this cause ASP.NET Core do a good job here, only with the wrong format. – Lion Jan 13 '17 at 20:27