How to control the CurrentCulture for recurring jobs in HangFire
Asked Answered
P

2

6

I'm using HangFire 1.6 in an asp.net WebApi2 solution with Owin.

The server is setup for one culture (da-DK) - I cannot change this. My application must use another culture (en-US) to correctly parse text data it receives. Basically I just want everything in my solution to use this other culture and never use the culture on the server.

In my web.config I have:

<system.web>
    <httpRuntime targetFramework="4.6.1" />
    <globalization enableClientBasedCulture="false" culture="en-US" uiCulture="en-US"/>
</system.web>

and in my Startup Configuration method I have

        CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CurrentCulture;
        CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CurrentUICulture;

because there are async methods and I want all threads started to share the same culture as specified in web.config

I also have some recurring jobs running in HangFire

My problem is that whatever I do the recurring jobs just keep using the server culture (da-DK) as CurrentCulture (the CurrentUICulture seems to react to what I do: HangFireDashboard

I've searched the Hangfire docs and SO, but I have found nothing that could solve this for me. I would rather avoid having to hardcode a culture in the top of every method used in a recurring job

Hope someone can help me...

Follow-up (2017-11-21):

It turns out this has nothing to do with HangFire. Apparently CultureInfo.CurrentCulture has not been set to the value provided in web.config when Startup.Configuration is run, hence CultureInfo.DefaultThreadCurrentCulture is set to a wrong value here. Once a controller executes the values are set to whatever is in the web.config.

For now I've decided to hard-code the culture in the Startup class to make sure it has been set on DefaultThreadCurrentCulture before any of the Hangfire recurring jobs execute

Pearle answered 20/11, 2017 at 16:22 Comment(3)
Asuming you are using SqlServer, what appears in the HangFire.JobParameter table as CurrentCulture and CurrentUICulture for the job (directly on the job entry if you are using redis) ?Brinson
@jbl: I believe that value you are referring to is the same that is being shown when you look at the job in the HangFire dashboard. Anyway - I see all of them having en-US (as I think they should) in the CurrentUICulture and most of them having da-DK (which they shouldn't) in CurrentCulture. A few, however, actually does have en-US as CurrentCulture. There seems to be no pattern as to when a worker will have en-US as CurrentCulturePearle
this may come from the client which enqueues the job. Anyway, as a first try, you may remove the CaptureCultureAttribute from the GlobalFilters : github.com/HangfireIO/Hangfire/blob/… then provide your own, hard coding the en-us value, github.com/HangfireIO/Hangfire/blob/…Brinson
C
4

We ran into this issue also, running on Azure, the culture of RecurringJob HangFire jobs were culture en-US. This lead to DateTime parse issues (amongst other things) as we expected a local culture as we had set <globalization culture="en-NZ" uiCulture="en-NZ" /> Sample job showing culture

If the RecurringJob was manually triggered (from HangFire web interface), or triggered from a controller (BackgroundJob.Enqueue), the culture was captured correctly.

To resolve, similar to the Follow-up (2017-11-21) above, we forced the culture to what was defined in the web.config in the Globalization attribute, prior to calling RecurringJob.AddOrUpdate.

We also found that still RecurringJob was using the wrong culture, so we had to replace the CaptureCultureAttribute with one that called ForceCulture first.

   public static void ConfigureHangfire(this IAppBuilder app)
        {
            //preamble removed <snip>

            app.UseHangfireServer();

         //force the culture objects to be loaded from system.web/globalization
            ForceCulture();

            //force remove the default CaptureCultureAttribute
Hangfire.GlobalJobFilters.Filters.Remove(Hangfire.GlobalJobFilters.Filters.Where(f => f.Instance is CaptureCultureAttribute).First());

            //add our one that forces the culture

            Hangfire.GlobalJobFilters.Filters.Add(new ForceCultureAttribute());

            //RecurringJob.AddOrUpdate...<snip>
        }

private static void ForceCulture()
        {
            //when the RecurringJob.AddOrUpdate runs, the CultureInfo.DefaultThreadCurrentCulture is not yet set, so RecurringJobs get the wrong culture
            //leading to DateTime parse issues & friends
            //https://mcmap.net/q/1783558/-how-to-control-the-currentculture-for-recurring-jobs-in-hangfire/647728
            GlobalizationSection globalizationConfig = (GlobalizationSection)ConfigurationManager.GetSection("system.web/globalization");
            var culture = new CultureInfo(globalizationConfig.Culture);
            var uiCulture = new CultureInfo(globalizationConfig.UICulture);
            CultureInfo.DefaultThreadCurrentCulture = culture;
            CultureInfo.DefaultThreadCurrentUICulture = uiCulture;
            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = uiCulture;
        }

//Taken directly from Hangfire source, with the exception of the call to ForceCulture inside OnCreating
//https://github.com/HangfireIO/Hangfire/blob/129707d66fde24dc6379fb9d6b15fa0b8ca48605/src/Hangfire.Core/CaptureCultureAttribute.cs

public sealed class ForceCultureAttribute : JobFilterAttribute, IClientFilter, IServerFilter
        {
            public void OnCreating(CreatingContext filterContext)
            {
                ForceCulture();

                if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));

                filterContext.SetJobParameter(
                    "CurrentCulture", CultureInfo.CurrentCulture.Name);
                filterContext.SetJobParameter(
                    "CurrentUICulture", CultureInfo.CurrentUICulture.Name);
            }

            public void OnCreated(CreatedContext filterContext)
            {
            }

            public void OnPerforming(PerformingContext filterContext)
            {
                var cultureName = filterContext.GetJobParameter<string>("CurrentCulture");
                var uiCultureName = filterContext.GetJobParameter<string>("CurrentUICulture");

                if (!String.IsNullOrEmpty(cultureName))
                {
                    filterContext.Items["PreviousCulture"] = CultureInfo.CurrentCulture;
                    SetCurrentCulture(new CultureInfo(cultureName));
                }

                if (!String.IsNullOrEmpty(uiCultureName))
                {
                    filterContext.Items["PreviousUICulture"] = CultureInfo.CurrentUICulture;
                    SetCurrentUICulture(new CultureInfo(uiCultureName));
                }
            }

            public void OnPerformed(PerformedContext filterContext)
            {
                if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));

                if (filterContext.Items.ContainsKey("PreviousCulture"))
                {
                    SetCurrentCulture((CultureInfo)filterContext.Items["PreviousCulture"]);
                }
                if (filterContext.Items.ContainsKey("PreviousUICulture"))
                {
                    SetCurrentUICulture((CultureInfo)filterContext.Items["PreviousUICulture"]);
                }
            }

            private static void SetCurrentCulture(CultureInfo value)
            {
#if NETFULL
            System.Threading.Thread.CurrentThread.CurrentCulture = value;
#else
                CultureInfo.CurrentCulture = value;
#endif
            }

            // ReSharper disable once InconsistentNaming
            private static void SetCurrentUICulture(CultureInfo value)
            {
#if NETFULL
            System.Threading.Thread.CurrentThread.CurrentUICulture = value;
#else
                CultureInfo.CurrentUICulture = value;
#endif
            }
        }

Hope this helps someone else in the future!

Christychristye answered 1/2, 2023 at 2:54 Comment(2)
A simpler way to remove the default CaptureCultureAttribute is by running GlobalJobFilters.Filters.Remove<CaptureCultureAttribute>();Knopp
Works very good!Calvin
P
0

From the source code the remove is abit tricky, so its actually the instance that needs to be removed like below: https://github.com/HangfireIO/Hangfire/blob/1ae167e3ffc87429a64cf95201c40508101cf25c/src/Hangfire.Core/Common/JobFilterCollection.cs#L96

var filter = GlobalJobFilters.Filters.OfType<JobFilter>().Where(c => c.Instance is CaptureCultureAttribute).FirstOrDefault().Instance;
            GlobalJobFilters.Filters.Remove(filter);


This will remove the capture culture all together, since in my oppinion it should not be there in the first place. Seems odd to have code that alter the curent culture instead of using whats actually configured.

So my startup is basically:

        static Startup()
        {
            
            var cultureInfo = CultureInfo.GetCultureInfo("en-US");
            Thread.CurrentThread.CurrentCulture = cultureInfo;
            Thread.CurrentThread.CurrentUICulture = cultureInfo;
  
      
            GlobalJobFilters.Filters.Remove(
                 GlobalJobFilters.Filters.OfType<JobFilter>().Where(c => c.Instance is CaptureCultureAttribute).FirstOrDefault().Instance
            );
          
      
        }

This makes the application for all purposes run with the coded culture, and you can just implement your logic to read from configuration file if needed.

Palaearctic answered 10/3, 2023 at 17:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.