0

We are using HangFire for one of our applications to schedule Reports.

CreateJobsOpenBidListReportHandler is class which is being inherited from AbstractCreateReportHandler class.

public class CreateJobsOpenBidListReportHandler : AbstractCreateReportHandler<CreateJobsOpenBidListReportCommand>
{
    public CreateJobsOpenBidListReportHandler(IJobManagementDBContext context, IMapper mapper,
        IJwtAuthenticationManager jwtAuthenticationManager, IErpRepository erpRepository, IStringLocalizer<Resource> localizer)
        : base(context, mapper, jwtAuthenticationManager, erpRepository, localizer)
    {
    }

    protected override ReportTypeEnum GetReportType()
    {
        return ReportTypeEnum.OPEN_BID_LIST;
    }

    protected override async Task<string> GenerateReportResults(string reportRequestJson, CancellationToken cancellationToken)
    {
        var selectionCriteria = JsonConvert.DeserializeObject<JobSelectionCriteria>(reportRequestJson);
        var jmasDbData = await ReportsProcessingRepository.GetJobsOpenBidListData(selectionCriteria, _context, cancellationToken);

        JobOpenBidListReportResultsDto responseDto = new();
        var resultsLines = responseDto.OpenBidListReportLines = _mapper.Map<List<JobOpenBidListReportResultLineDto>>(jmasDbData);

        // Query for all unique BillTo Customers
        var customerResults = resultsLines.Select(
            jobLine =>
                jobLine.BillToCustomerList.Select(cusLine => cusLine)
            ).SelectMany(cusLine => cusLine); // Flattens the list
        var customerErpIds = customerResults.Select(cusLine => cusLine.ErpId).Distinct();
        ErpCustomersReportHelper customerRecordsHelper = await ErpCustomersReportHelper.GetHelperInstance(customerErpIds, _erpRepo, cancellationToken);

        // Populate unmapped, derivative data
        foreach (var jobResultLine in resultsLines)
        {
            customerRecordsHelper.PopulateBillToCustomers(jobResultLine.BillToCustomerList,_erpRepo);
        }

        return JsonConvert.SerializeObject(responseDto);
    }
}

And This is AbstractCreateReportHandler class

public abstract class AbstractCreateReportHandler<TCreateCommand> : IRequestHandler<TCreateCommand, ReportStatusDto> where TCreateCommand : IRequest<ReportStatusDto>
{
    protected readonly IJobManagementDBContext _context;
    protected readonly IMapper _mapper;
    protected readonly IJwtAuthenticationManager _jwtAuthenticationManager;
    protected readonly IReportBackgroundQueue _reportQueue;
    protected readonly IErpRepository _erpRepo;
    protected readonly IStringLocalizer<Resource> _localizer;

    // This class must be able to provide any service that might be used in generating a Report
    protected AbstractCreateReportHandler(IJobManagementDBContext context, IMapper mapper,
        IJwtAuthenticationManager jwtAuthenticationManager, IErpRepository erpRepo, IStringLocalizer<Resource> localizer)
    {
        _context = context;
        _mapper = mapper;
        _jwtAuthenticationManager = jwtAuthenticationManager;
        _erpRepo = erpRepo;
        _localizer = localizer;
    }

    // Returns the implementation's Report type
    protected abstract ReportTypeEnum GetReportType();

    // NOTE: Override this method with the actual specifics of your Report generation
    // It must take the serialized request for the report, and then return the serialized results.
    // These results will go into the DB's [Report].[Result] column, and must deserialize into the contents of the <xxxReportResponse><Report> property of its corresponding GET endpoint response
    protected abstract Task<string> GenerateReportResults(string reportRequestJson, CancellationToken cancellationToken);


    /********************** COMMON  REPORT  FUNCTIONALITY ****************************/

    public async Task<ReportStatusDto> Handle(TCreateCommand request, CancellationToken cancellationToken)
    {
        string requestString = JsonConvert.SerializeObject(request);
        ReportStatusDto response = new ReportStatusDto();
        CommonScheduleCommand scheduleDetails =  checkForScheduleDetails(request, cancellationToken);
        if (scheduleDetails!= null)
        {
            //Update this in the report schedule table
            string userId = _jwtAuthenticationManager.GetUser();
            ReportScheduleDetails details = _mapper.Map<ReportScheduleDetails>(scheduleDetails);
            details.CreatedByUser = userId;
            details.CreateDate = DateTimeOffset.Now;
            details.ReportTypeFK = Convert.ToInt32(scheduleDetails.ReportType);
            details.ReportScheduleTypeFK = Convert.ToInt32(scheduleDetails.ScheduleType);
            _context.ReportScheduleDetails.Add(details);
            await _context.SaveChangesAsync(cancellationToken);
            //Create an identifier for the newly created schedule.
            string reportId = details.Id.ToString();
            RecurringJob.AddOrUpdate(reportId+5, ()=> CreateNewReport(requestString, cancellationToken), "*/1 * * * *");
            response = new ReportStatusDto {ScheduleId = details.Id.ToString() };
        }
        else
        {
            response = await CreateNewReport(requestString, cancellationToken);
        }
        return response;
    }
    public async Task<ReportStatusDto> CreateNewReport(string requestString,CancellationToken cancellationToken)
    {
        // Initialize report record
        Report reportRecord = new(requestString, GetReportType(), _jwtAuthenticationManager.GetUser());
        await ReportsRepository.CreateReport(reportRecord, true, _context, cancellationToken);

        // Run the report synchronously and wait for it
        await RunReport(reportRecord.Id, cancellationToken);

        // Return updated report record
        Report finishedReportRecord = _context.Reports.AsNoTracking()
            .Include(report => report.ReportStateFKNavigation)
            .FirstOrDefault(r => r.Id == reportRecord.Id);
        ReportStatusDto responseDto = _mapper.Map<ReportStatusDto>(finishedReportRecord);

        return responseDto;
    }

    private CommonScheduleCommand checkForScheduleDetails(TCreateCommand request, CancellationToken cancellationToken)
    {
        string requestString = JsonConvert.SerializeObject(request);
        Type type = request.GetType();
        CommonScheduleCommand response = null;
        if(type == typeof(CreateJobsOpenBidListReportCommand))
        {
            CreateJobsOpenBidListReportCommand requestObject = JsonConvert.DeserializeObject<CreateJobsOpenBidListReportCommand>(requestString);
            response = requestObject.ScheduleDetails;

        }
        return response;
    }

    private async Task RunReport(int reportId, CancellationToken cancellationToken)
    {
        var reportRecord = (await ReportsRepository.GetReports(r => r.Id == reportId, _context, cancellationToken)).FirstOrDefault();
        if (reportRecord is null)
        {
            // Report not found
            // TODO: throw an error
            return;
        }

        // First, update the report to a RUNNING status
        await ReportsRepository.UpdateStartedReport(reportId, _context, cancellationToken);

        // Generate the report results
        string results;
        try
        {
            results = await GenerateReportResults(reportRecord.Request, cancellationToken);
        }
        catch (Exception ex)
        {
            // Report failed
            Exception innerException = ex;
            while (ex.InnerException is not null)
            {
                ex = ex.InnerException;
            }

            results = innerException.Message;
            await ReportsRepository.UpdateReportWithError(reportRecord, results, _context, cancellationToken);
            return;
        }

        // Report succeeded
        await ReportsRepository.UpdateFinishedReport(reportId, results, _context, cancellationToken);
    }
}

Every class which is inherited from 'AbstractCreateReportHandler' has GenerateReportResults() method to generate its won report.

For every report request that comes through , Handle method in 'AbstractCreateReportHandler' is executed first which internally calls CreateNewReport(..) method. This method internally calls 'GenerateReportResults()' of respective Report's classes(CreateJobsOpenBidListReportHandler in this case)

So this 'CreateNewReport()' is marked to be added as Recurring method that runs as per CRON.

But HangFire stores CreateNewReport() method as part of CreateJobsOpenBidListReportHandler.cs and as it is showing InvalidOperationException

{"t":"Application.ApplicationBusiness.Reports.JobOpenBidList.Commands.CreateJobsOpenBidListReport.CreateJobsOpenBidListReportHandler, Application","m":"CreateNewReport","p":["System.String","System.Threading.CancellationToken, mscorlib"]}

HangFire throws InvalidOperationException as shown below

System.InvalidOperationException: The type Application.ApplicationBusiness.Reports.JobOpenBidList.Commands.CreateJobsOpenBidListReport.CreateJobsOpenBidListReportHandler does not contain a method with signature `CreateNewReport(String, CancellationToken)

RashmiMs
  • 129
  • 14

0 Answers0