HangfireIO / Hangfire

An easy way to perform background job processing in .NET and .NET Core applications. No Windows Service or separate process required
https://www.hangfire.io
Other
9.43k stars 1.71k forks source link

Mixing .NET Framework and .NET Core #2400

Closed terryaney closed 5 months ago

terryaney commented 6 months ago

I have an abnormal (temporary) environment. My original implementation was a Windows Service hosting Hangfire as the job processing host, written in .NET Framework.

Then, I had .NET Framework sites/apis that used something like:

var hangfireId = Hangfire.BackgroundJob.Enqueue(
      () => new JobInvoker().Invoke(
             jobName,
             inputPackage,
             null,
             HF.JobCancellationToken.Null )
);

To perform a fire/forget job.

We are slowly migrating everything to .NET Core, but need to be in 'mixed' environment until migration is complete. I was hoping to upgrade only an API to .NET Core and have it leverage the existing .NET Framework Windows Service.

In my .NET Core Api project, I referenced my .NET Framework assembly containing JobInvoker and use the same line as above. My intention was for it to simply schedule the job into the Hangfire SQL db and my .NET Framework Windows Service would pick it up and process it. But after Enqueue call, the job fails with this exception:

{ "FailedAt": "2024-05-06T20:49:03.0362835Z", "ExceptionType": "System.IO.FileNotFoundException", "ExceptionMessage": "Could not load file or assembly 'System.Private.Xml.Linq, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies. The system cannot find the file specified.", "ExceptionDetails": " System.IO.FileNotFoundException: Could not load file or assembly 'System.Private.Xml.Linq, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies. The system cannot find the file specified.       File name: 'System.Private.Xml.Linq, PublicKeyToken=cc7b13ffcd2ddd51'        at System.Reflection.RuntimeAssembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)        at System.Reflection.RuntimeAssembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)        at System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, Evidence assemblySecurity, RuntimeAssembly reqAssembly, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)        at System.Reflection.Assembly.Load(AssemblyName assemblyRef)        at Hangfire.Common.TypeHelper.AssemblyResolver(String assemblyString)        at System.Collections.Concurrent.ConcurrentDictionary2.GetOrAdd(TKey key, Func2 valueFactory)        at Hangfire.Common.TypeHelper.CachedAssemblyResolver(AssemblyName assemblyName)        at System.TypeNameParser.ResolveAssembly(String asmName, Func2 assemblyResolver, Boolean throwOnError, StackCrawlMark& stackMark)        at System.TypeNameParser.ConstructType(Func2 assemblyResolver, Func4 typeResolver, Boolean throwOnError, Boolean ignoreCase, StackCrawlMark& stackMark)        at System.TypeNameParser.GetType(String typeName, Func2 assemblyResolver, Func4 typeResolver, Boolean throwOnError, Boolean ignoreCase, StackCrawlMark& stackMark)        at System.Type.GetType(String typeName, Func2 assemblyResolver, Func4 typeResolver, Boolean throwOnError)        at Hangfire.Common.TypeHelper.DefaultTypeResolver(String typeName)        at System.Linq.Enumerable.WhereSelectArrayIterator2.MoveNext()        at System.Linq.Buffer1..ctor(IEnumerable1 source)        at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)        at Hangfire.Storage.InvocationData.DeserializeJob()              === Pre-bind state information ===       LOG: DisplayName = System.Private.Xml.Linq, PublicKeyToken=cc7b13ffcd2ddd51        (Partial)       WRN: Partial binding information was supplied for an assembly:       WRN: Assembly Name: System.Private.Xml.Linq, PublicKeyToken=cc7b13ffcd2ddd51 | Domain ID: 1       WRN: A partial bind occurs when only part of the assembly display name is provided.       WRN: This might result in the binder loading an incorrect assembly.       WRN: It is recommended to provide a fully specified textual identity for the assembly,       WRN: that consists of the simple name, version, culture, and public key token.       WRN: See whitepaper http://go.microsoft.com/fwlink/?LinkId=109270 for more information and common solutions to this issue.       LOG: Appbase = file:///C:/BTR/Evolution/Services/BTR.Evolution.Service/bin/Debug/       LOG: Initial PrivatePath = NULL       Calling assembly : Hangfire.Core, Version=1.7.30.0, Culture=neutral, PublicKeyToken=null.       ===       LOG: This bind starts in default load context.       LOG: Using application configuration file: C:\BTR\Evolution\Services\BTR.Evolution.Service\bin\Debug\BTR.Evolution.Service.exe.Config       LOG: Using host configuration file:       LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.       LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).       LOG: The same bind was seen before, and was failed with hr = 0x80070002." }

It seems like it is trying to resolve .NET Core LINQ to Xml objects? I could be wrong, but the namespace of 'System.Private.Xml.Linq' was not something I was used to seeing in .NET Framework.

Is there a way to do what I'm trying to do? Or a workaround I can perform for this non-standard setup I'm stuck in for a while?

odinserj commented 6 months ago

A lot of logic was added to ensure smooth co-existence of .NET Framework and .NET Core servers at the same time, to be able to create and consume jobs from each other. However, I don't remember anything regarding Linq, since these types aren't something usually used as background job parameters.

How does your background job method look like, can you send its full signature, including parameter types you are using to reproduce the behavior?

terryaney commented 6 months ago

This Hangfire implementation/framework of ours was written 10 years ago maybe, lol. Looking at my JobInvoker method and trying to figure out why some of the things done are being done, but here is the signature:

public void Invoke( string jobName, XElement inputPackage, PerformContext performContext, IJobCancellationToken cancellationToken )

I'm not in a position to be able to modify that. I could maybe add a string inputPackage overload if that would help?

odinserj commented 6 months ago

I'm not in a position to be able to modify that. I could maybe add a string inputPackage overload if that would help?

Perhaps this would be the best solution as for now, since contents of an XElement is string, but I will check the binding anyway.

terryaney commented 6 months ago

FYI...probably totally going against what I am supposed to do but hopefully temporary. Having weird issue with integration testing with my mix of Core and Framework. With all the problems I'm having, until I can I migrate to .NET Core completely I think I'm going to just directly inject the row into the Hangfire Sql DB then let the HF server of mine pick up the job. How crazy am I? lol

terryaney commented 6 months ago

Here's what I ended up doing as a work around until I properly move everything to latest/greatest .NET Core/HangFire

        var qb = cn.QueryBuilder( $@"
            set xact_abort on; 
            set nocount on; 
            declare @jobId bigint;
            declare @stateId bigint;
            begin tran;

            insert into HangFire.Job (InvocationData, Arguments, CreatedAt, ExpireAt) 
            values ({invocationData:nvarchar(max)}, {arguments:nvarchar(max)}, {now}, DATEADD(day, 1, {now}));

            select @jobId = scope_identity(); 

            insert into HangFire.JobParameter (JobId, Name, Value) 
            values (@jobId, 'CurrentCulture', '""en-US""'), (@jobId, 'CurrentUICulture', '""en-US""');

            insert into HangFire.State (JobId, Name, CreatedAt, Data) 
            values (@jobId, 'Enqueued', {now}, {state:nvarchar(max)})

            select @stateId = scope_identity(); 

            UPDATE HangFire.Job 
            SET StateId = @stateId, StateName = 'Enqueued'
            WHERE Id = @jobId

            INSERT INTO HangFire.JobQueue (JobId, Queue) VALUES (@jobId, 'default' )

            commit tran;

            SELECT @jobId
        " );

        var hangfireId = await qb.QueryFirstAsync<int>( cancellationToken: c );
terryaney commented 5 months ago

Peeked at commit. Looks like you would support my scenario natively now?

odinserj commented 5 months ago

Yep, it works now, the problem happened when old .net framework tried to deserialize a type from the new one.

New one had necessary bindings to handle from the old one, but not the vice versa. I just added one more redirect, so should work now.

Hope will solve weird problems with the new release next week.