HangFire использует класс, отличный от того, в котором представлен метод для повторяющихся заданий.
Мы используем HangFire для одного из наших приложений для планирования отчетов.
CreateJobsOpenBidListReportHandler — это класс, наследуемый от класса AbstractCreateReportHandler.
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);
}
}
И это класс AbstractCreateReportHandler
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);
}
}
Каждый класс, унаследованный от «AbstractCreateReportHandler», имеет метод GenerateReportResults() для создания отчета о выигрыше.
Для каждого запроса отчета, поступающего через , сначала выполняется метод Handle в «AbstractCreateReportHandler», который внутренне вызывает метод CreateNewReport(..). Этот метод внутренне вызывает «GenerateReportResults()» соответствующих классов отчетов (в данном случае CreateJobsOpenBidListReportHandler)
Таким образом, этот «CreateNewReport ()» помечен как повторяющийся метод, который работает в соответствии с CRON.
Но HangFire хранит метод CreateNewReport() как часть CreateJobsOpenBidListReportHandler.cs и показывает InvalidOperationException
{"t":"Application.ApplicationBusiness.Reports.JobOpenBidList.Commands.CreateJobsOpenBidListReport.CreateJobsOpenBidListReportHandler, Application","m":"CreateNewReport","p":["System.String","System.Threading.CancellationToken, mscorlib "]}
HangFire выдает InvalidOperationException, как показано ниже.
System.InvalidOperationException: Тип
Application.ApplicationBusiness.Reports.JobOpenBidList.Commands.CreateJobsOpenBidListReport.CreateJobsOpenBidListReportHandler
не содержит метод с сигнатурой `CreateNewReport(String, CancellationToken)