Home > Back-end >  Type casting with generics
Type casting with generics

Time:12-09

I have created following abstraction for scheduling jobs:

public abstract class BaseJob
{
    public string? JobId { get; set; }
}

public interface IJobData
{ }

public interface IJob<in TJobData> where TJobData : IJobData
{
    Task ExecuteAsync(TJobData jobData);
}

I create jobs using a factory:

public class JobCreator<TJob, TJobData>
    where TJob : IJob<TJobData>, new()
    where TJobData : IJobData
{
    public async Task ExecuteAsync(TJobData jobData)
    {
        var job = new TJob();
        await job.ExecuteAsync(jobData);
    }
}

Jobs are scheduled like this:

JobClient.Enqueue<JobCreator<ForgotPasswordJob, ForgotPasswordJobData>>(job => job.ExecuteAsync(jobData));

The scheduler will dehydrate the job to a job store and hydrate it later. I can hook into the hydration process and print the type:

Application.Common.Interfaces.JobCreator`2[Application.Jobs.ForgotPasswordJob,Application.Jobs.ForgotPasswordJobData]

Inside the hook I have access to the hydrated job

var job = schedulerHydrationMagic...

The variable job is of object type at this point. Is it possible to cast it, to get access to the JobCreator or even better ForgotPasswordJob? I want to set JobId inside BaseJob.

I tried

var job = schedulerMagic as JobCreator<IJob<IJobData>, IJobData>

but type checking complains that IJob must be a non abstract type.

CodePudding user response:

JobCreator has a generic new() constraint for the generic TJob argument:

public class JobCreator<TJob, TJobData>
    where TJob : IJob<TJobData>, new() // <- here
    where TJobData : IJobData
{
    public async Task ExecuteAsync(TJobData jobData)
    {
        var job = new TJob();
        await job.ExecuteAsync(jobData);
    }
}

It's apparent that you did that so that you could call new TJob().

But it explains why the first generic argument - TJob - must always be a non-abstract type.

If the generic type of TJobData is IJob<IJobData>, then at runtime that line of code would equate to

var job = new IJob<IJobData>();

...which doesn't make sense. You can't call new on an interface (or on an abstract class.) This is hard for us to spot, but it's easy for the compiler.

The point of strongly typed code is that the compiler detects things like this when the app is compiled. Otherwise the code could look okay, you could run it, and then at runtime it discovers that TJob doesn't have a default constructor. It's better to have the compiler reject it and make us figure it out than to think it's okay and then get an even more confusing runtime error.


There is no simple answer to this because the problem is right in the middle of the design. This is what I call The Generic Rabbit Hole of Madness. It's not derogatory - I've done it myself.

My first recommendation isn't to figure out how to make the generics work - it's to stop trying to do what you're doing. Are there really numerous possible implementations of this? From the code it looks like there's only one. So by trying to write a generic version that will handle unknown future types you're likely trying to solve a problem you don't have. In that case the answer is easy. Don't try to solve it. Just write code to schedule whatever the thing is you need to schedule without all the generics.

If you must do this, then what you need is to get rid of the new() constraint. That means you won't be able to call new TJob(). Instead you'll need to create some sort of factory interface like:

public interface IJobFactory
{
    IJob<TJobData> Create<TJobData>() where TJobData : IJobData
}

and then your JobCreator looks like this:

public class JobCreator<TJob, TJobData>
    where TJob : IJob<TJobData>
    where TJobData : IJobData
{
    private readonly IJobFactory _jobFactory;

    public JobCreator(IJobFactory jobFactory)
    {
        _jobFactory = jobFactory;
    }

    public async Task ExecuteAsync(TJobData jobData)
    {
        var job = _jobFactory.Create<TJobData>();
        await job.ExecuteAsync(jobData);
    }
}

But this hasn't solved the problem. It's just moved it. Now you have to figure out how to implement IJobFactory. (This is already confusing because now JobCreator doesn't really create a job - IJobFactory does.)

This is why I call it a rabbit hole. It just goes on forever. If there's any way you can modify the code to get rid of the problem instead of trying to solve it, that's better.

Once you get the code you need working without all the confusion it's likely that a better solution to the larger problem - if there even is a problem - will come into focus.

CodePudding user response:

I guess you need a further interface to get a Job from the scheduler. The factory interface can't be used, it has new constraint.

  • Related