<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Mon, 05 Oct 2026 20:02:43 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=Jadyn" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/tong-guo-silkierquartzguan-li-quartz-ziyd4ias</guid>
      <link>https://abp.io/community/posts/tong-guo-silkierquartzguan-li-quartz-ziyd4ias</link>
      <a10:author>
        <a10:name>Jadyn</a10:name>
        <a10:uri>https://abp.io/community/members/Jadyn</a10:uri>
      </a10:author>
      <category>SilkierQuartz</category>
      <category>background-job</category>
      <category>background-worker</category>
      <title>通过SilkierQuartz管理Quartz</title>
      <description>本文展示了如何使用SilkierQuartz实现类似Hangfire的功能丰富的作业管理和执行历史仪表板.</description>
      <pubDate>Fri, 16 Jun 2023 09:49:09 Z</pubDate>
      <a10:updated>2026-10-05T19:01:10Z</a10:updated>
      <content:encoded><![CDATA[<h1>通过SilkierQuartz管理Quartz</h1>
<h2>介绍</h2>
<p>abp framework支持使用<strong>Quartz.Net</strong>作为<strong>BackgroundJob</strong>和<strong>BackgroundWorker</strong>的实现. Quartz.Net拥有比Hangfire有更好的CRON格式支持（支持秒），更强大的触发器，日历等。但遗憾的是它没有类似Hangfire开箱即用的仪表板. 本文将借助<a href="https://github.com/maikebing/SilkierQuartz">SilkierQuartz</a>使你将可以获得类似的体验.</p>
<p><strong>SilkierQuartz</strong>由<a href="https://github.com/jlucansky/Quartzmin">Quartzmin</a>发展而来.</p>
<p>通过<code>SilkierQuartz</code>让我们拥有一个功能丰富的Darkboard, 管理我们的Job和执行历史.</p>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/demo.gif" alt="SilkierQuartz" /></p>
<h2>初始化项目</h2>
<ol>
<li>创建项目, 然后运行<code>DbMigrator</code></li>
</ol>
<pre><code class="language-bash">abp new Abp.SilkierQuartzDemo -t app
</code></pre>
<ol start="2">
<li>添加Quartz的<strong>BackgroundJob</strong>和<strong>BackgroundWorker</strong>实现包到<code>Abp.SilkierQuartzDemo.Web</code></li>
</ol>
<pre><code class="language-bash">abp add-package Volo.Abp.BackgroundJobs.Quartz
abp add-package Volo.Abp.BackgroundWorkers.Quartz
dotnet add package Quartz.Serialization.Json --version 3.4.0 # Match Volo.Abp.Quartz's Quartz version
</code></pre>
<ol start="3">
<li>配置Quartz以使用SqlServer持久化作业</li>
</ol>
<pre><code class="language-csharp">public override void PreConfigureServices(ServiceConfigurationContext context)
{
    var configuration = context.Services.GetConfiguration();

    ...

    PreConfigure&lt;AbpQuartzOptions&gt;(options =&gt;
    {
        options.Configurator = configure =&gt;
        {
            configure.UsePersistentStore(storeOptions =&gt;
            {
                storeOptions.UseProperties = true;
                storeOptions.UseJsonSerializer();
                storeOptions.UseSqlServer(configuration.GetConnectionString(&quot;Default&quot;)!);
                storeOptions.UseClustering(c =&gt;
                {
                    c.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
                    c.CheckinInterval = TimeSpan.FromSeconds(10);
                });
            });
        };
    });
}
</code></pre>
<blockquote>
<p>详细配置请查看<a href="https://docs.abp.io/en/abp/latest/Background-Jobs-Quartz#configuration">文档</a></p>
</blockquote>
<blockquote>
<p>选择你的对于的数据库类型, 初始化Quartz数据表. https://github.com/quartznet/quartznet/tree/main/database/tables</p>
</blockquote>
<h2>安装SilkierQuartz</h2>
<ol>
<li>安装<code>SilkierQuartz</code>到<code>Abp.SilkierQuartzDemo.Web</code></li>
</ol>
<pre><code class="language-bash">dotnet add package SilkierQuartz --version 5.0.356
</code></pre>
<ol start="2">
<li>添加<code>AbpSilkierQuartzExtensions.cs</code>到<code>Abp.SilkierQuartzDemo.Web</code></li>
</ol>
<pre><code class="language-csharp">public static class AbpSilkierQuartzExtensions
{
    private const string DefaultVirtualPathRoot = &quot;/quartz&quot;;

    public static IApplicationBuilder UseAbpSilkierQuartz(
        this IApplicationBuilder app,
        Action&lt;SilkierQuartz.SilkierQuartzOptions&gt;? actionOptions = null)
    {
        var options = new SilkierQuartz.SilkierQuartzOptions()
        {
            Logo = app.ApplicationServices.GetRequiredService&lt;IBrandingProvider&gt;().LogoUrl ?? &quot;Content/Images/logo.png&quot;,
            Scheduler = app.ApplicationServices.GetRequiredService&lt;IScheduler&gt;(),
            VirtualPathRoot = DefaultVirtualPathRoot,
            UseLocalTime = true,
            DefaultDateFormat = &quot;yyyy-MM-dd&quot;,
            DefaultTimeFormat = &quot;HH:mm:ss&quot;,
            CronExpressionOptions = new CronExpressionDescriptor.Options()
            {
                DayOfWeekStartIndexZero = false //Quartz uses 1-7 as the range
            }
        };

        actionOptions?.Invoke(options);

        var fsOptions = new FileServerOptions()
        {
            RequestPath = new PathString($&quot;{options.VirtualPathRoot}/Content&quot;),
            EnableDefaultFiles = false,
            EnableDirectoryBrowsing = false,
            FileProvider = new EmbeddedFileProvider(typeof(SilkierQuartz.SilkierQuartzOptions).Assembly, &quot;SilkierQuartz.Content&quot;)
        };

        app.UseFileServer(fsOptions);

        var services = SilkierQuartz.Services.Create(options, null);

        app.Use(async (context, next) =&gt;
        {
            context.Items[typeof(SilkierQuartz.Services)] = services;
            await next();
        });

        return app;
    }

    public static void MapAbpSilkierQuartz(this IEndpointRouteBuilder routeBuilder, string virtualPathRoot = DefaultVirtualPathRoot)
    {
        routeBuilder.MapControllerRoute(nameof(SilkierQuartz), $&quot;{virtualPathRoot}/{{controller=Scheduler}}/{{action=Index}}&quot;);
    }
}
</code></pre>
<ol start="3">
<li>配置<code>ProjectNameWebModule</code>, 对<code>SilkierQuartz</code>控制器跳过AntiForgery验证, 配置路由</li>
</ol>
<pre><code class="language-csharp">...
public class SilkierQuartzDemoWebModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {   
        ...
        Configure&lt;AbpAntiForgeryOptions&gt;(options =&gt;
        {
            options.AutoValidateFilter = type =&gt; !type.FullName!.StartsWith(&quot;SilkierQuartz.Controllers&quot;);
        });
    }

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        ...
        app.UseAbpSilkierQuartz();
        app.UseConfiguredEndpoints(endpoints =&gt;
        {
            endpoints.MapAbpSilkierQuartz();
        });
    }
}
</code></pre>
<ol start="4">
<li>配置权限<code>ProjectNamePermissionDefinitionProvider</code></li>
</ol>
<pre><code class="language-csharp">public override void Define(IPermissionDefinitionContext context)
{
    var myGroup = context.AddGroup(SilkierQuartzDemoPermissions.GroupName);

    // 权限名称必须是'SilkierQuartz'
    myGroup.AddPermission(&quot;SilkierQuartz&quot;, L(&quot;Permission:SilkierQuartz&quot;));
    ...
}
</code></pre>
<ol start="5">
<li><p>运行<code>DbMigrator</code></p>
</li>
<li><p>启动并访问, 'https://your-host/quartz'. 现在你可以基本预览<strong>SilkierQuartz</strong></p>
</li>
</ol>
<h2>实现ExecutionHistory持久化</h2>
<p>默认的SilkierQuartz的执行记录使用内存实现. 我们可以通过abp实现数据库持久化.
由于代码较多, 下文仅展示重要部分, 详细信息请参考<a href="https://github.com/JadynWong/Abp.SilkierQuartzDemo/commit/45667f59a8d8d250397068242eebd2dd511ef118">提交</a></p>
<ol>
<li>添加<code>SilkierQuartz.Plugins.RecentHistory</code>到<code>Abp.SilkierQuartzDemo.Domain</code></li>
</ol>
<pre><code class="language-bash">dotnet add package SilkierQuartz.Plugins.RecentHistory
</code></pre>
<ol start="2">
<li>在<code>Abp.SilkierQuartzDemo.Domain</code>创建Quartz目录后, 添加实体<code>QuartzJobSummary</code>和<code>QuartzExecutionHistory</code></li>
</ol>
<pre><code class="language-csharp">public class QuartzExecutionHistory : BasicAggregateRoot&lt;Guid&gt;
{
    public string FireInstanceId { get; protected set; } = null!;

    public string SchedulerInstanceId { get; set; } = null!;

    public string SchedulerName { get; set; } = null!;

    public string? Job { get; set; }

    public string? Trigger { get; set; }

    public DateTime? ScheduledFireTimeUtc { get; set; }

    public DateTime ActualFireTimeUtc { get; set; }

    public bool Recovering { get; set; }

    public bool Vetoed { get; set; }

    public DateTime? FinishedTimeUtc { get; set; }

    public string? ExceptionMessage { get; set; }

    protected QuartzExecutionHistory()
    {

    }

    public QuartzExecutionHistory(Guid id, string fireInstanceId) : base(id)
    {
        FireInstanceId = fireInstanceId;
    }
}

public class QuartzJobSummary : BasicAggregateRoot&lt;Guid&gt;
{
    public string SchedulerName { get; protected set; } = null!;

    public int TotalJobsExecuted { get; set; }

    public int TotalJobsFailed { get; set; }

    protected QuartzJobSummary()
    {

    }

    public QuartzJobSummary(Guid id, string schedulerName) : base(id)
    {
        SchedulerName = schedulerName;
    }
}

</code></pre>
<ol start="3">
<li>添加<code>IQuartzExecutionHistoryRepository</code>和<code>IQuartzJobSummaryRepository</code>接口, 然后在EntityFrameworkCore中实现.</li>
</ol>
<pre><code class="language-csharp">public interface IQuartzExecutionHistoryRepository : IBasicRepository&lt;QuartzExecutionHistory, Guid&gt;
{
    Task&lt;QuartzExecutionHistory?&gt; FindByFireInstanceIdAsync(string fireInstanceId, CancellationToken cancellationToken = default);

    Task&lt;List&lt;QuartzExecutionHistory&gt;&gt; GetLastOfEveryJobAsync(string schedulerName, int limitPerJob, CancellationToken cancellationToken = default);

    Task&lt;List&lt;QuartzExecutionHistory&gt;&gt; GetLastOfEveryTriggerAsync(
        string schedulerName,
        int limitPerTrigger,
        int skipPerTrigger = 0,
        CancellationToken cancellationToken = default);

    Task&lt;List&lt;QuartzExecutionHistory&gt;&gt; GetLastAsync(string schedulerName, int limit, CancellationToken cancellationToken = default);

    Task PurgeAsync(CancellationToken cancellationToken = default);
}

public interface IQuartzJobSummaryRepository : IBasicRepository&lt;QuartzJobSummary, Guid&gt;
{
    Task&lt;QuartzJobSummary?&gt; FindBySchedulerNameAsync(string schedulerName, CancellationToken cancellationToken = default);

    Task&lt;int&gt; GetTotalJobsExecutedAsync(string schedulerName, CancellationToken cancellationToken = default);

    Task&lt;int&gt; GetTotalJobsFailedAsync(string schedulerName, CancellationToken cancellationToken = default);

    Task IncrementTotalJobsExecutedAsync(string schedulerName, CancellationToken cancellationToken = default);

    Task IncrementTotalJobsFailedAsync(string schedulerName, CancellationToken cancellationToken = default);
}
</code></pre>
<ol start="4">
<li>配置<code>ProjectNameDbContext</code></li>
</ol>
<pre><code class="language-csharp">protected override void OnModelCreating(ModelBuilder builder)
{
    ...
    builder.Entity&lt;QuartzExecutionHistory&gt;(b =&gt;
    {
        b.ToTable(&quot;QuartzExecutionHistories&quot;, SilkierQuartzDemoConsts.DbSchema);
        b.ConfigureByConvention();

        /* Configure more properties here */
        b.Property(x =&gt; x.FireInstanceId).HasMaxLength(200);
        b.Property(x =&gt; x.SchedulerInstanceId).HasMaxLength(200);
        b.Property(x =&gt; x.SchedulerName).HasMaxLength(200);
        b.Property(x =&gt; x.Job).HasMaxLength(300);
        b.Property(x =&gt; x.Trigger).HasMaxLength(300);

        b.HasIndex(x =&gt; x.FireInstanceId);
    });

    builder.Entity&lt;QuartzJobSummary&gt;(b =&gt;
    {
        b.ToTable(&quot;QuartzJobSummares&quot;, SilkierQuartzDemoConsts.DbSchema);
        b.ConfigureByConvention();

        /* Configure more properties here */
        b.Property(x =&gt; x.SchedulerName).HasMaxLength(200);
    });
}
</code></pre>
<ol start="5">
<li>添加<code>AbpExecutionHistoryStore</code>以实现<code>IExecutionHistoryStore</code></li>
</ol>
<pre><code class="language-csharp">public class AbpExecutionHistoryStore : IExecutionHistoryStore, ISingletonDependency
{
    public string SchedulerName { get; set; } = null!;

    protected IServiceScopeFactory ServiceScopeFactory = null!;

    public ILogger&lt;AbpExecutionHistoryStore&gt; Logger { get; set; }

    public AbpExecutionHistoryStore()
    {
        Logger = NullLogger&lt;AbpExecutionHistoryStore&gt;.Instance;
    }

    public AbpExecutionHistoryStore(
        IServiceScopeFactory serviceScopeFactory) : this()
    {
        ServiceScopeFactory = serviceScopeFactory;
    }

    public async Task&lt;ExecutionHistoryEntry?&gt; Get(string fireInstanceId)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistory = await repository.FindByFireInstanceIdAsync(fireInstanceId);
        if (quartzJobHistory == null)
        {
            return null;
        }
        return quartzJobHistory.ToEntry();
    }

    public async Task Purge()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        await repository.PurgeAsync();
    }

    public async Task Save(ExecutionHistoryEntry entry)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var guidGenerator = scope.ServiceProvider.GetRequiredService&lt;IGuidGenerator&gt;();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistory = await repository.FindByFireInstanceIdAsync(entry.FireInstanceId);
        if (quartzJobHistory == null)
        {
            quartzJobHistory = entry.ToEntity(new QuartzExecutionHistory(guidGenerator.Create(), entry.FireInstanceId));
            await repository.InsertAsync(quartzJobHistory);
        }
        else
        {
            quartzJobHistory = entry.ToEntity(quartzJobHistory);
            await repository.UpdateAsync(quartzJobHistory);
        }
    }

    public async Task&lt;IEnumerable&lt;ExecutionHistoryEntry&gt;&gt; FilterLastOfEveryJob(int limitPerJob)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistories = await repository.GetLastOfEveryJobAsync(SchedulerName, limitPerJob);

        return quartzJobHistories.Select(x =&gt; x.ToEntry());
    }

    public async Task&lt;IEnumerable&lt;ExecutionHistoryEntry&gt;&gt; FilterLastOfEveryTrigger(int limitPerTrigger)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistories = await repository.GetLastOfEveryTriggerAsync(SchedulerName, limitPerTrigger);

        return quartzJobHistories.Select(x =&gt; x.ToEntry());
    }

    public async Task&lt;IEnumerable&lt;ExecutionHistoryEntry&gt;&gt; FilterLast(int limit)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistories = await repository.GetLastAsync(SchedulerName, limit);

        return quartzJobHistories.Select(x =&gt; x.ToEntry());
    }

    public async Task&lt;int&gt; GetTotalJobsExecuted()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();

        return await repository.GetTotalJobsExecutedAsync(SchedulerName);
    }

    public async Task&lt;int&gt; GetTotalJobsFailed()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();

        return await repository.GetTotalJobsFailedAsync(SchedulerName);
    }

    public async Task IncrementTotalJobsExecuted()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();

        await repository.IncrementTotalJobsExecutedAsync(SchedulerName);
    }

    public async Task IncrementTotalJobsFailed()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();

        await repository.IncrementTotalJobsFailedAsync(SchedulerName);
    }

    public virtual async Task InitializeSummaryAsync()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var unitOfWorkManager = scope.ServiceProvider.GetRequiredService&lt;IUnitOfWorkManager&gt;();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();
        var guidGenerator = scope.ServiceProvider.GetRequiredService&lt;IGuidGenerator&gt;();

        using var uow = unitOfWorkManager.Begin(true);

        var quartzJobSummary = await repository.FindBySchedulerNameAsync(SchedulerName);
        if (quartzJobSummary == null)
        {
            quartzJobSummary = new QuartzJobSummary(guidGenerator.Create(), SchedulerName);
            await repository.InsertAsync(quartzJobSummary);
        }

        await uow.CompleteAsync();
    }
}
</code></pre>
<ol start="6">
<li>添加<code>AbpExecutionHistoryPlugin</code>以实现<code>ISchedulerPlugin</code>, <code>IJobListener</code></li>
</ol>
<pre><code class="language-csharp">public class AbpExecutionHistoryPlugin : ISchedulerPlugin, IJobListener
{
    private IScheduler _scheduler = null!;
    private IExecutionHistoryStore _store = null!;

    public Type StoreType { get; set; } = null!;

    public string Name { get; protected set; } = string.Empty;

    public Task Initialize(string pluginName, IScheduler scheduler, CancellationToken cancellationToken = default)
    {
        Name = pluginName;
        _scheduler = scheduler;
        _scheduler.ListenerManager.AddJobListener(this, EverythingMatcher&lt;JobKey&gt;.AllJobs());

        return Task.FromResult(0);
    }

    public async Task Start(CancellationToken cancellationToken = default)
    {
        _store = _scheduler.Context.GetExecutionHistoryStore();

        if (_store == null)
        {
            throw new AbpException(nameof(StoreType) + &quot; is not set.&quot;);
        }

        _store.SchedulerName = _scheduler.SchedulerName;

        if (_store is AbpExecutionHistoryStore abpStore)
            await abpStore.InitializeSummaryAsync();

        await _store.Purge();
    }

    public Task Shutdown(CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public async Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        var entry = new ExecutionHistoryEntry()
        {
            FireInstanceId = context.FireInstanceId,
            SchedulerInstanceId = context.Scheduler.SchedulerInstanceId,
            SchedulerName = context.Scheduler.SchedulerName,
            ActualFireTimeUtc = context.FireTimeUtc.UtcDateTime,
            ScheduledFireTimeUtc = context.ScheduledFireTimeUtc?.UtcDateTime,
            Recovering = context.Recovering,
            Job = context.JobDetail.Key.ToString(),
            Trigger = context.Trigger.Key.ToString(),
        };
        await _store.Save(entry);
    }

    public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default)
    {
        var entry = await _store.Get(context.FireInstanceId);
        if (entry != null)
        {
            entry.FinishedTimeUtc = DateTime.UtcNow;
            entry.ExceptionMessage = jobException?.GetBaseException()?.ToString();
        }
        else
        {
            entry = new ExecutionHistoryEntry()
            {
                FireInstanceId = context.FireInstanceId,
                SchedulerInstanceId = context.Scheduler.SchedulerInstanceId,
                SchedulerName = context.Scheduler.SchedulerName,
                ActualFireTimeUtc = context.FireTimeUtc.UtcDateTime,
                ScheduledFireTimeUtc = context.ScheduledFireTimeUtc?.UtcDateTime,
                Recovering = context.Recovering,
                Job = context.JobDetail.Key.ToString(),
                Trigger = context.Trigger.Key.ToString(),
                FinishedTimeUtc = DateTime.UtcNow,
                ExceptionMessage = jobException?.GetBaseException()?.ToString()
            };
        }
        await _store.Save(entry);

        if (jobException == null)
            await _store.IncrementTotalJobsExecuted();
        else
            await _store.IncrementTotalJobsFailed();
    }

    public async Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        var entry = await _store.Get(context.FireInstanceId);
        if (entry != null)
        {
            entry.Vetoed = true;
            await _store.Save(entry);
        }
    }
}
</code></pre>
<ol start="7">
<li>添加<code>ExecutionHistoryCleanBackgroundWorker</code>后台工作者, 用于清理执行历史</li>
</ol>
<pre><code class="language-csharp">public class ExecutionHistoryCleanBackgroundWorker : AsyncPeriodicBackgroundWorkerBase, ITransientDependency
{
    public ExecutionHistoryCleanBackgroundWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory)
        : base(timer, serviceScopeFactory)
    {
        Timer.Period = 1000 * 60; //1 minutes
    }

    protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext)
    {
        Logger.LogDebug(&quot;ExecutionHistoryCleanBackgroundWorker executing&quot;);

        await LazyServiceProvider
            .LazyGetRequiredService&lt;AbpExecutionHistoryStore&gt;()
            .Purge();

        Logger.LogDebug(&quot;ExecutionHistoryCleanBackgroundWorker executed&quot;);
    }
}
</code></pre>
<ol start="8">
<li>注册<code>ExecutionHistoryCleanBackgroundWorker</code>, 配置<code>ExecutionHistoryStore</code></li>
</ol>
<pre><code class="language-csharp">public class SilkierQuartzDemoDomainModule : AbpModule
{
    ...
    public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
    {
        var scheduler = context.ServiceProvider.GetRequiredService&lt;IScheduler&gt;();
        var executionHistoryStore = context.ServiceProvider.GetRequiredService&lt;AbpExecutionHistoryStore&gt;();
        scheduler.Context.SetExecutionHistoryStore(executionHistoryStore);
    }

    public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
    {
        await base.OnApplicationInitializationAsync(context);

        await context.AddBackgroundWorkerAsync&lt;ExecutionHistoryCleanBackgroundWorker&gt;();
    }
}
</code></pre>
<ol start="9">
<li>修改<code>AbpQuartzOptions</code>配置以注册我们的持久化实现</li>
</ol>
<pre><code class="language-diff">PreConfigure&lt;AbpQuartzOptions&gt;(options =&gt;
{
    options.Configurator = configure =&gt;
    {
+       configure.SetProperty(&quot;quartz.plugin.recentHistory.type&quot;, typeof(AbpExecutionHistoryPlugin).AssemblyQualifiedNameWithoutVersion());
+       configure.SetProperty(&quot;quartz.plugin.recentHistory.storeType&quot;, typeof(AbpExecutionHistoryStore).AssemblyQualifiedNameWithoutVersion());
        configure.UsePersistentStore(storeOptions =&gt;
        {
            storeOptions.UseProperties = true;
            storeOptions.UseJsonSerializer();
            storeOptions.UseSqlServer(configuration.GetConnectionString(&quot;Default&quot;)!);
            storeOptions.UseClustering(c =&gt;
            {
                c.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
                c.CheckinInterval = TimeSpan.FromSeconds(10);
            });
        });
    };
});
</code></pre>
<ol start="10">
<li>在EntityFramework项目生成迁移记录并应用</li>
</ol>
<pre><code class="language-bash">dotnet ef migrations add Added_QuartzRecentHistory
dotnet ef database update
</code></pre>
<ol start="11">
<li>启动并访问, 'https://your-host/quartz/History'. 现在你可以查看执行历史记录.</li>
</ol>
<h2>预览</h2>
<h3>Overview</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/preview1.png" alt="preview1" /></p>
<h3>Jobs</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/preview2.png" alt="preview2" /></p>
<h3>Triggers</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/preview3.png" alt="preview3" /></p>
<h3>Hostory</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/preview4.png" alt="preview4" /></p>
<p>示例 https://github.com/JadynWong/Abp.SilkierQuartzDemo</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/manage-quartz-with-silkierquartz-xb4ovbj9</guid>
      <link>https://abp.io/community/posts/manage-quartz-with-silkierquartz-xb4ovbj9</link>
      <a10:author>
        <a10:name>Jadyn</a10:name>
        <a10:uri>https://abp.io/community/members/Jadyn</a10:uri>
      </a10:author>
      <category>SilkierQuartz</category>
      <category>background-job</category>
      <category>background-worker</category>
      <title>Manage Quartz with SilkierQuartz</title>
      <description>This article show how to achieve a feature-rich job management and execution history dashboard similar to Hangfire using SilkierQuartz.</description>
      <pubDate>Fri, 16 Jun 2023 09:48:14 Z</pubDate>
      <a10:updated>2026-10-05T16:02:29Z</a10:updated>
      <content:encoded><![CDATA[<h1>Manage Quartz with SilkierQuartz</h1>
<h2>Introduction</h2>
<p>The ABP Framework supports using <strong>Quartz.Net</strong> as an implementation for <strong>BackgroundJob</strong> and <strong>BackgroundWorker</strong>. Quartz.Net offers better CRON format support (including seconds), more powerful triggers, calendars, and so on compared to Hangfire. However, unfortunately, it does not come with a ready-to-use dashboard like Hangfire does. This article will introduce how you can achieve a similar experience by utilizing <a href="https://github.com/maikebing/SilkierQuartz">SilkierQuartz</a>.</p>
<p><strong>SilkierQuartz</strong> based from <a href="https://github.com/jlucansky/Quartzmin">Quartzmin</a>.</p>
<p>With SilkierQuartz, we gain access to a feature-rich Dashboard to manage our jobs and execution history.</p>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/demo.gif" alt="SilkierQuartz" /></p>
<h2>Initialization project</h2>
<ol>
<li>Create the project, then run <code>DbMigrator</code></li>
</ol>
<pre><code class="language-bash">abp new Abp.SilkierQuartzDemo -t app
</code></pre>
<ol start="2">
<li>Add Quartz's <strong>BackgroundJob</strong> and <strong>BackgroundWorker</strong> implementation packages to <code>Abp.SilkierQuartzDemo.Web</code>.</li>
</ol>
<pre><code class="language-bash">abp add-package Volo.Abp.BackgroundJobs.Quartz
abp add-package Volo.Abp.BackgroundWorkers.Quartz
dotnet add package Quartz.Serialization.Json --version 3.4.0 # Match Volo.Abp.Quartz's Quartz version
</code></pre>
<ol start="3">
<li>Configuring Quartz to use SqlServer persistence jobs</li>
</ol>
<pre><code class="language-csharp">public override void PreConfigureServices(ServiceConfigurationContext context)
{
    var configuration = context.Services.GetConfiguration();

    ...

    PreConfigure&lt;AbpQuartzOptions&gt;(options =&gt;
    {
        options.Configurator = configure =&gt;
        {
            configure.UsePersistentStore(storeOptions =&gt;
            {
                storeOptions.UseProperties = true;
                storeOptions.UseJsonSerializer();
                storeOptions.UseSqlServer(configuration.GetConnectionString(&quot;Default&quot;)!);
                storeOptions.UseClustering(c =&gt;
                {
                    c.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
                    c.CheckinInterval = TimeSpan.FromSeconds(10);
                });
            });
        };
    });
}
</code></pre>
<blockquote>
<p>For detailed configuration, please see <a href="https://docs.abp.io/en/abp/latest/Background-Jobs-Quartz#configuration">documentation</a></p>
</blockquote>
<blockquote>
<p>Select your database type, initialize the Quartz database table. https://github.com/quartznet/quartznet/tree/main/database/tables</p>
</blockquote>
<h2>Installing SilkierQuartz</h2>
<ol>
<li>Install <code>SilkierQuartz</code> to <code>Abp.SilkierQuartzDemo.Web</code></li>
</ol>
<pre><code class="language-bash">dotnet add package SilkierQuartz --version 5.0.356
</code></pre>
<ol start="2">
<li>Add <code>AbpSilkierQuartzExtensions.cs</code> to <code>Abp.SilkierQuartzDemo.Web</code></li>
</ol>
<pre><code class="language-csharp">public static class AbpSilkierQuartzExtensions
{
    private const string DefaultVirtualPathRoot = &quot;/quartz&quot;;

    public static IApplicationBuilder UseAbpSilkierQuartz(
        this IApplicationBuilder app,
        Action&lt;SilkierQuartz.SilkierQuartzOptions&gt;? actionOptions = null)
    {
        var options = new SilkierQuartz.SilkierQuartzOptions()
        {
            Logo = app.ApplicationServices.GetRequiredService&lt;IBrandingProvider&gt;().LogoUrl ?? &quot;Content/Images/logo.png&quot;,
            Scheduler = app.ApplicationServices.GetRequiredService&lt;IScheduler&gt;(),
            VirtualPathRoot = DefaultVirtualPathRoot,
            UseLocalTime = true,
            DefaultDateFormat = &quot;yyyy-MM-dd&quot;,
            DefaultTimeFormat = &quot;HH:mm:ss&quot;,
            CronExpressionOptions = new CronExpressionDescriptor.Options()
            {
                DayOfWeekStartIndexZero = false //Quartz uses 1-7 as the range
            }
        };

        actionOptions?.Invoke(options);

        var fsOptions = new FileServerOptions()
        {
            RequestPath = new PathString($&quot;{options.VirtualPathRoot}/Content&quot;),
            EnableDefaultFiles = false,
            EnableDirectoryBrowsing = false,
            FileProvider = new EmbeddedFileProvider(typeof(SilkierQuartz.SilkierQuartzOptions).Assembly, &quot;SilkierQuartz.Content&quot;)
        };

        app.UseFileServer(fsOptions);

        var services = SilkierQuartz.Services.Create(options, null);

        app.Use(async (context, next) =&gt;
        {
            context.Items[typeof(SilkierQuartz.Services)] = services;
            await next();
        });

        return app;
    }

    public static void MapAbpSilkierQuartz(this IEndpointRouteBuilder routeBuilder, string virtualPathRoot = DefaultVirtualPathRoot)
    {
        routeBuilder.MapControllerRoute(nameof(SilkierQuartz), $&quot;{virtualPathRoot}/{{controller=Scheduler}}/{{action=Index}}&quot;);
    }
}
</code></pre>
<ol start="3">
<li>Configure <code>ProjectNameWebModule</code>, skip anti-forgery validation for <code>SilkierQuartz</code> controllers, configure routing.</li>
</ol>
<pre><code class="language-csharp">...
public class SilkierQuartzDemoWebModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {   
        ...
        Configure&lt;AbpAntiForgeryOptions&gt;(options =&gt;
        {
            options.AutoValidateFilter = type =&gt; !type.FullName!.StartsWith(&quot;SilkierQuartz.Controllers&quot;);
        });
    }

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        ...
        app.UseAbpSilkierQuartz();
        app.UseConfiguredEndpoints(endpoints =&gt;
        {
            endpoints.MapAbpSilkierQuartz();
        });
    }
}
</code></pre>
<ol start="4">
<li>Configure permission in <code>ProjectNamePermissionDefinitionProvider</code></li>
</ol>
<pre><code class="language-csharp">public override void Define(IPermissionDefinitionContext context)
{
    var myGroup = context.AddGroup(SilkierQuartzDemoPermissions.GroupName);

    // Permission name must be 'SilkierQuartz'
    myGroup.AddPermission(&quot;SilkierQuartz&quot;, L(&quot;Permission:SilkierQuartz&quot;));
    ...
}
</code></pre>
<ol start="5">
<li><p>Run <code>DbMigrator</code></p>
</li>
<li><p>Start and visit, 'https://your-host/quartz'. Now you can get a basic preview of <strong>SilkierQuartz</strong>.</p>
</li>
</ol>
<h2>Implementation of ExecutionHistory persistence</h2>
<p>The default SilkierQuartz execution hostory is implemented using in-memory. We can implement database persistence via abp.
Due to the large amount of code, only the important parts are shown below, for details please refer to <a href="https://github.com/JadynWong/Abp.SilkierQuartzDemo/commit/45667f59a8d8d250397068242eebd2dd511ef118">commit</a></p>
<ol>
<li>Add <code>SilkierQuartz.Plugins.RecentHistory</code> to <code>Abp.SilkierQuartzDemo.Domain</code></li>
</ol>
<pre><code class="language-bash">dotnet add package SilkierQuartz.Plugins.RecentHistory
</code></pre>
<ol start="2">
<li>Creating the Quartz directory in <code>Abp.SilkierQuartzDemo.Domain</code>, add the entities <code>QuartzJobSummary</code> and <code>QuartzExecutionHistory</code></li>
</ol>
<pre><code class="language-csharp">public class QuartzExecutionHistory : BasicAggregateRoot&lt;Guid&gt;
{
    public string FireInstanceId { get; protected set; } = null!;

    public string SchedulerInstanceId { get; set; } = null!;

    public string SchedulerName { get; set; } = null!;

    public string? Job { get; set; }

    public string? Trigger { get; set; }

    public DateTime? ScheduledFireTimeUtc { get; set; }

    public DateTime ActualFireTimeUtc { get; set; }

    public bool Recovering { get; set; }

    public bool Vetoed { get; set; }

    public DateTime? FinishedTimeUtc { get; set; }

    public string? ExceptionMessage { get; set; }

    protected QuartzExecutionHistory()
    {

    }

    public QuartzExecutionHistory(Guid id, string fireInstanceId) : base(id)
    {
        FireInstanceId = fireInstanceId;
    }
}

public class QuartzJobSummary : BasicAggregateRoot&lt;Guid&gt;
{
    public string SchedulerName { get; protected set; } = null!;

    public int TotalJobsExecuted { get; set; }

    public int TotalJobsFailed { get; set; }

    protected QuartzJobSummary()
    {

    }

    public QuartzJobSummary(Guid id, string schedulerName) : base(id)
    {
        SchedulerName = schedulerName;
    }
}

</code></pre>
<ol start="3">
<li>Add <code>IQuartzExecutionHistoryRepository</code> and <code>IQuartzJobSummaryRepository</code> interfaces, and implement them in EntityFrameworkCore.</li>
</ol>
<pre><code class="language-csharp">public interface IQuartzExecutionHistoryRepository : IBasicRepository&lt;QuartzExecutionHistory, Guid&gt;
{
    Task&lt;QuartzExecutionHistory?&gt; FindByFireInstanceIdAsync(string fireInstanceId, CancellationToken cancellationToken = default);

    Task&lt;List&lt;QuartzExecutionHistory&gt;&gt; GetLastOfEveryJobAsync(string schedulerName, int limitPerJob, CancellationToken cancellationToken = default);

    Task&lt;List&lt;QuartzExecutionHistory&gt;&gt; GetLastOfEveryTriggerAsync(
        string schedulerName,
        int limitPerTrigger,
        int skipPerTrigger = 0,
        CancellationToken cancellationToken = default);

    Task&lt;List&lt;QuartzExecutionHistory&gt;&gt; GetLastAsync(string schedulerName, int limit, CancellationToken cancellationToken = default);

    Task PurgeAsync(CancellationToken cancellationToken = default);
}

public interface IQuartzJobSummaryRepository : IBasicRepository&lt;QuartzJobSummary, Guid&gt;
{
    Task&lt;QuartzJobSummary?&gt; FindBySchedulerNameAsync(string schedulerName, CancellationToken cancellationToken = default);

    Task&lt;int&gt; GetTotalJobsExecutedAsync(string schedulerName, CancellationToken cancellationToken = default);

    Task&lt;int&gt; GetTotalJobsFailedAsync(string schedulerName, CancellationToken cancellationToken = default);

    Task IncrementTotalJobsExecutedAsync(string schedulerName, CancellationToken cancellationToken = default);

    Task IncrementTotalJobsFailedAsync(string schedulerName, CancellationToken cancellationToken = default);
}
</code></pre>
<ol start="4">
<li>Configure <code>ProjectNameDbContext</code></li>
</ol>
<pre><code class="language-csharp">protected override void OnModelCreating(ModelBuilder builder)
{
    ...
    builder.Entity&lt;QuartzExecutionHistory&gt;(b =&gt;
    {
        b.ToTable(&quot;QuartzExecutionHistories&quot;, SilkierQuartzDemoConsts.DbSchema);
        b.ConfigureByConvention();

        /* Configure more properties here */
        b.Property(x =&gt; x.FireInstanceId).HasMaxLength(200);
        b.Property(x =&gt; x.SchedulerInstanceId).HasMaxLength(200);
        b.Property(x =&gt; x.SchedulerName).HasMaxLength(200);
        b.Property(x =&gt; x.Job).HasMaxLength(300);
        b.Property(x =&gt; x.Trigger).HasMaxLength(300);

        b.HasIndex(x =&gt; x.FireInstanceId);
    });

    builder.Entity&lt;QuartzJobSummary&gt;(b =&gt;
    {
        b.ToTable(&quot;QuartzJobSummares&quot;, SilkierQuartzDemoConsts.DbSchema);
        b.ConfigureByConvention();

        /* Configure more properties here */
        b.Property(x =&gt; x.SchedulerName).HasMaxLength(200);
    });
}
</code></pre>
<ol start="5">
<li>Add <code>AbpExecutionHistoryStore</code> to implement <code>IExecutionHistoryStore</code>.</li>
</ol>
<pre><code class="language-csharp">public class AbpExecutionHistoryStore : IExecutionHistoryStore, ISingletonDependency
{
    public string SchedulerName { get; set; } = null!;

    protected IServiceScopeFactory ServiceScopeFactory = null!;

    public ILogger&lt;AbpExecutionHistoryStore&gt; Logger { get; set; }

    public AbpExecutionHistoryStore()
    {
        Logger = NullLogger&lt;AbpExecutionHistoryStore&gt;.Instance;
    }

    public AbpExecutionHistoryStore(
        IServiceScopeFactory serviceScopeFactory) : this()
    {
        ServiceScopeFactory = serviceScopeFactory;
    }

    public async Task&lt;ExecutionHistoryEntry?&gt; Get(string fireInstanceId)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistory = await repository.FindByFireInstanceIdAsync(fireInstanceId);
        if (quartzJobHistory == null)
        {
            return null;
        }
        return quartzJobHistory.ToEntry();
    }

    public async Task Purge()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        await repository.PurgeAsync();
    }

    public async Task Save(ExecutionHistoryEntry entry)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var guidGenerator = scope.ServiceProvider.GetRequiredService&lt;IGuidGenerator&gt;();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistory = await repository.FindByFireInstanceIdAsync(entry.FireInstanceId);
        if (quartzJobHistory == null)
        {
            quartzJobHistory = entry.ToEntity(new QuartzExecutionHistory(guidGenerator.Create(), entry.FireInstanceId));
            await repository.InsertAsync(quartzJobHistory);
        }
        else
        {
            quartzJobHistory = entry.ToEntity(quartzJobHistory);
            await repository.UpdateAsync(quartzJobHistory);
        }
    }

    public async Task&lt;IEnumerable&lt;ExecutionHistoryEntry&gt;&gt; FilterLastOfEveryJob(int limitPerJob)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistories = await repository.GetLastOfEveryJobAsync(SchedulerName, limitPerJob);

        return quartzJobHistories.Select(x =&gt; x.ToEntry());
    }

    public async Task&lt;IEnumerable&lt;ExecutionHistoryEntry&gt;&gt; FilterLastOfEveryTrigger(int limitPerTrigger)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistories = await repository.GetLastOfEveryTriggerAsync(SchedulerName, limitPerTrigger);

        return quartzJobHistories.Select(x =&gt; x.ToEntry());
    }

    public async Task&lt;IEnumerable&lt;ExecutionHistoryEntry&gt;&gt; FilterLast(int limit)
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzExecutionHistoryRepository&gt;();

        var quartzJobHistories = await repository.GetLastAsync(SchedulerName, limit);

        return quartzJobHistories.Select(x =&gt; x.ToEntry());
    }

    public async Task&lt;int&gt; GetTotalJobsExecuted()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();

        return await repository.GetTotalJobsExecutedAsync(SchedulerName);
    }

    public async Task&lt;int&gt; GetTotalJobsFailed()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();

        return await repository.GetTotalJobsFailedAsync(SchedulerName);
    }

    public async Task IncrementTotalJobsExecuted()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();

        await repository.IncrementTotalJobsExecutedAsync(SchedulerName);
    }

    public async Task IncrementTotalJobsFailed()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();

        await repository.IncrementTotalJobsFailedAsync(SchedulerName);
    }

    public virtual async Task InitializeSummaryAsync()
    {
        using var scope = ServiceScopeFactory.CreateScope();
        var unitOfWorkManager = scope.ServiceProvider.GetRequiredService&lt;IUnitOfWorkManager&gt;();
        var repository = scope.ServiceProvider.GetRequiredService&lt;IQuartzJobSummaryRepository&gt;();
        var guidGenerator = scope.ServiceProvider.GetRequiredService&lt;IGuidGenerator&gt;();

        using var uow = unitOfWorkManager.Begin(true);

        var quartzJobSummary = await repository.FindBySchedulerNameAsync(SchedulerName);
        if (quartzJobSummary == null)
        {
            quartzJobSummary = new QuartzJobSummary(guidGenerator.Create(), SchedulerName);
            await repository.InsertAsync(quartzJobSummary);
        }

        await uow.CompleteAsync();
    }
}
</code></pre>
<ol start="6">
<li>Add <code>AbpExecutionHistoryPlugin</code> to implement <code>ISchedulerPlugin</code>, <code>IJobListener</code></li>
</ol>
<pre><code class="language-csharp">public class AbpExecutionHistoryPlugin : ISchedulerPlugin, IJobListener
{
    private IScheduler _scheduler = null!;
    private IExecutionHistoryStore _store = null!;

    public Type StoreType { get; set; } = null!;

    public string Name { get; protected set; } = string.Empty;

    public Task Initialize(string pluginName, IScheduler scheduler, CancellationToken cancellationToken = default)
    {
        Name = pluginName;
        _scheduler = scheduler;
        _scheduler.ListenerManager.AddJobListener(this, EverythingMatcher&lt;JobKey&gt;.AllJobs());

        return Task.FromResult(0);
    }

    public async Task Start(CancellationToken cancellationToken = default)
    {
        _store = _scheduler.Context.GetExecutionHistoryStore();

        if (_store == null)
        {
            throw new AbpException(nameof(StoreType) + &quot; is not set.&quot;);
        }

        _store.SchedulerName = _scheduler.SchedulerName;

        if (_store is AbpExecutionHistoryStore abpStore)
            await abpStore.InitializeSummaryAsync();

        await _store.Purge();
    }

    public Task Shutdown(CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public async Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        var entry = new ExecutionHistoryEntry()
        {
            FireInstanceId = context.FireInstanceId,
            SchedulerInstanceId = context.Scheduler.SchedulerInstanceId,
            SchedulerName = context.Scheduler.SchedulerName,
            ActualFireTimeUtc = context.FireTimeUtc.UtcDateTime,
            ScheduledFireTimeUtc = context.ScheduledFireTimeUtc?.UtcDateTime,
            Recovering = context.Recovering,
            Job = context.JobDetail.Key.ToString(),
            Trigger = context.Trigger.Key.ToString(),
        };
        await _store.Save(entry);
    }

    public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default)
    {
        var entry = await _store.Get(context.FireInstanceId);
        if (entry != null)
        {
            entry.FinishedTimeUtc = DateTime.UtcNow;
            entry.ExceptionMessage = jobException?.GetBaseException()?.ToString();
        }
        else
        {
            entry = new ExecutionHistoryEntry()
            {
                FireInstanceId = context.FireInstanceId,
                SchedulerInstanceId = context.Scheduler.SchedulerInstanceId,
                SchedulerName = context.Scheduler.SchedulerName,
                ActualFireTimeUtc = context.FireTimeUtc.UtcDateTime,
                ScheduledFireTimeUtc = context.ScheduledFireTimeUtc?.UtcDateTime,
                Recovering = context.Recovering,
                Job = context.JobDetail.Key.ToString(),
                Trigger = context.Trigger.Key.ToString(),
                FinishedTimeUtc = DateTime.UtcNow,
                ExceptionMessage = jobException?.GetBaseException()?.ToString()
            };
        }
        await _store.Save(entry);

        if (jobException == null)
            await _store.IncrementTotalJobsExecuted();
        else
            await _store.IncrementTotalJobsFailed();
    }

    public async Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        var entry = await _store.Get(context.FireInstanceId);
        if (entry != null)
        {
            entry.Vetoed = true;
            await _store.Save(entry);
        }
    }
}
</code></pre>
<ol start="7">
<li>Add <code>ExecutionHistoryCleanBackgroundWorker</code> background worker, used to clean up execution history</li>
</ol>
<pre><code class="language-csharp">public class ExecutionHistoryCleanBackgroundWorker : AsyncPeriodicBackgroundWorkerBase, ITransientDependency
{
    public ExecutionHistoryCleanBackgroundWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory)
        : base(timer, serviceScopeFactory)
    {
        Timer.Period = 1000 * 60; //1 minutes
    }

    protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext)
    {
        Logger.LogDebug(&quot;ExecutionHistoryCleanBackgroundWorker executing&quot;);

        await LazyServiceProvider
            .LazyGetRequiredService&lt;AbpExecutionHistoryStore&gt;()
            .Purge();

        Logger.LogDebug(&quot;ExecutionHistoryCleanBackgroundWorker executed&quot;);
    }
}
</code></pre>
<ol start="8">
<li>Register <code>ExecutionHistoryCleanBackgroundWorker</code> and configure <code>ExecutionHistoryStore</code>.</li>
</ol>
<pre><code class="language-csharp">public class SilkierQuartzDemoDomainModule : AbpModule
{
    ...
    public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
    {
        var scheduler = context.ServiceProvider.GetRequiredService&lt;IScheduler&gt;();
        var executionHistoryStore = context.ServiceProvider.GetRequiredService&lt;AbpExecutionHistoryStore&gt;();
        scheduler.Context.SetExecutionHistoryStore(executionHistoryStore);
    }

    public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
    {
        await base.OnApplicationInitializationAsync(context);

        await context.AddBackgroundWorkerAsync&lt;ExecutionHistoryCleanBackgroundWorker&gt;();
    }
}
</code></pre>
<ol start="9">
<li>Modify the <code>AbpQuartzOptions</code> configuration to register our persistence implementation</li>
</ol>
<pre><code class="language-diff">PreConfigure&lt;AbpQuartzOptions&gt;(options =&gt;
{
    options.Configurator = configure =&gt;
    {
+       configure.SetProperty(&quot;quartz.plugin.recentHistory.type&quot;, typeof(AbpExecutionHistoryPlugin).AssemblyQualifiedNameWithoutVersion());
+       configure.SetProperty(&quot;quartz.plugin.recentHistory.storeType&quot;, typeof(AbpExecutionHistoryStore).AssemblyQualifiedNameWithoutVersion());
        configure.UsePersistentStore(storeOptions =&gt;
        {
            storeOptions.UseProperties = true;
            storeOptions.UseJsonSerializer();
            storeOptions.UseSqlServer(configuration.GetConnectionString(&quot;Default&quot;)!);
            storeOptions.UseClustering(c =&gt;
            {
                c.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
                c.CheckinInterval = TimeSpan.FromSeconds(10);
            });
        });
    };
});
</code></pre>
<ol start="10">
<li>Add migration in EntityFramework project and apply them to database</li>
</ol>
<pre><code class="language-bash">dotnet ef migrations add Added_QuartzRecentHistory
dotnet ef database update
</code></pre>
<ol start="11">
<li>Start and visit 'https://your-host/quartz/History'. Now you can view the execution history.</li>
</ol>
<h2>Preview</h2>
<h3>Overview</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/preview1.png" alt="preview1" /></p>
<h3>Jobs</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/preview2.png" alt="preview2" /></p>
<h3>Triggers</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/preview3.png" alt="preview3" /></p>
<h3>Hostory</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-15-Manage-quartz-with-silkier-quartz/media/preview4.png" alt="preview4" /></p>
<p>Demo https://github.com/JadynWong/Abp.SilkierQuartzDemo</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/ru-he-shi-yong-rapidocti-huan-swaggerui-8me2lvvd</guid>
      <link>https://abp.io/community/posts/ru-he-shi-yong-rapidocti-huan-swaggerui-8me2lvvd</link>
      <a10:author>
        <a10:name>Jadyn</a10:name>
        <a10:uri>https://abp.io/community/members/Jadyn</a10:uri>
      </a10:author>
      <category>swagger</category>
      <category>RapiDoc</category>
      <title>如何使用RapiDoc替换SwaggerUI</title>
      <description>本文展示了如何用RapiDoc取代SwaggerUI作为abp中的默认API文档页.</description>
      <pubDate>Thu, 08 Jun 2023 10:36:32 Z</pubDate>
      <a10:updated>2026-10-05T09:32:28Z</a10:updated>
      <content:encoded><![CDATA[<h1>如何使用RapiDoc替换SwaggerUI</h1>
<h2>介绍</h2>
<p>abp使用非常流行的 SwaggerUI 作为默认api文档页面. 但是如果你对它的UI感到厌倦, 可以尝试使用 <a href="https://rapidocweb.com/">RapiDoc</a> 替换Swagger-UI.</p>
<p>RapiDoc提供与SwaggerUI几乎相同的能力, 但是他的UI更加漂亮, 可定制化更高.</p>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-08-How-to-replace-swagger-ui-with-rapi-doc/media/rapidoc.png" alt="RapiDoc" /></p>
<p>如果你对此感兴趣, 请继续阅读, 了解如何使用 <a href="https://rapidocweb.com/">RapiDoc</a> 作为您项目的文档页面.</p>
<h2>创建一个新项目</h2>
<p>在这里我使用单层模板进行演示, 对其他模板步骤相同. 你也可以在现有项目上完成后续步骤.</p>
<pre><code class="language-bash">abp new RapiDocDemo -t app-nolayers
</code></pre>
<h2>安装RapiDoc</h2>
<p>感谢<a href="https://github.com/luoyunchong">luoyunchong</a>创建的<a href="https://github.com/luoyunchong/IGeekFan.AspNetCore.RapiDoc">IGeekFan.AspNetCore.RapiDoc</a>项目, 让我们刚容易使用RapiDoc.</p>
<p>让我们将RapiDoc添加到Web项目.</p>
<pre><code class="language-bash">dotnet add package IGeekFan.AspNetCore.RapiDoc
</code></pre>
<h2>创建abp.rapi-doc.js</h2>
<p>在Web项目创建文件夹<code>wwwroot/rapi-doc</code>, 并使用以下内容创建<code>abp.rapi-doc.js</code>文件.</p>
<pre><code class="language-javascript">var abp = abp || {};

(function () {
    window.addEventListener('DOMContentLoaded', (event) =&gt; {
        var excludeUrl = [&quot;swagger.json&quot;, &quot;connect/token&quot;]
        var firstRequest = true;

        /*
          Ensure that the DOM is loaded, then add the event listener.
          here we are listenig to 'before-try' event which fires when the user clicks
          on TRY, it then modifies the POST requests by adding a custom header
        */
        const rapidocEl = document.getElementById('thedoc');
        rapidocEl.addEventListener('before-try', async (e) =&gt; {
            var request = e.detail.request;
            if (request.url.includes(excludeUrl[1])) {
                firstRequest = true;
            }

            if (firstRequest &amp;&amp; !excludeUrl.some(url =&gt; request.url.includes(url))) {
                await fetch(`${abp.appPath}abp/Swashbuckle/SetCsrfCookie`, {
                    headers: request.headers
                });
                firstRequest = false;
            }

            var antiForgeryToken = abp.security.antiForgery.getToken();
            if (antiForgeryToken) {
                request.headers.append(abp.security.antiForgery.tokenHeaderName, antiForgeryToken);
            }
        });
    });
})();
</code></pre>
<h2>配置RapiDoc</h2>
<p>配置RapiDocDemoModule.</p>
<h3>步骤1. 添加using.</h3>
<pre><code class="language-csharp">using IGeekFan.AspNetCore.RapiDoc;
</code></pre>
<h3>步骤2. 配置<code>RapiDoc</code>.</h3>
<p>按如下所示修改<code>ConfigureSwaggerServices</code>来配置<code>RapiDocOptions</code>.</p>
<pre><code class="language-diff">private void ConfigureSwaggerServices(IServiceCollection services, IConfiguration configuration)
{
+   Configure&lt;RapiDocOptions&gt;(options =&gt;
+   {
+       options.RoutePrefix = &quot;rapi-doc&quot;;//access url

+       // optional
+       options.GenericRapiConfig.RenderStyle = &quot;focused&quot;;// view | read | focused
+       options.GenericRapiConfig.Theme = &quot;dark&quot;; // light | dark
+       options.GenericRapiConfig.SchemaStyle = &quot;tree&quot;; // table | tree
+       options.GenericRapiConfig.ShowMethodInNavBar = &quot;as-colored-text&quot;; // false | as-plain-text | as-colored-text | as-colored-block
+       options.GenericRapiConfig.UsePathInNavBar = true;
+
+       // required
+       options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;RapiDocDemo API&quot;);
+       options.InjectJavascript(&quot;/swagger/ui/abp.js&quot;);
+       options.InjectJavascript(&quot;/rapi-doc/abp.rapi-doc.js&quot;);
+   });
    services.AddAbpSwaggerGen(
        options =&gt;
        {
            options.SwaggerDoc(&quot;v1&quot;, new OpenApiInfo { Title = &quot;Blogs API&quot;, Version = &quot;v1&quot; });
            options.DocInclusionPredicate((docName, description) =&gt; true);
            options.CustomSchemaIds(type =&gt; type.FullName);
        }
    );
}
</code></pre>
<blockquote>
<p>更多配置请参考<a href="https://rapidocweb.com/api.html">https://rapidocweb.com/api.html</a>.</p>
</blockquote>
<p>对于Web项目, 直接看<a href="#%E6%AD%A5%E9%AA%A43-%E4%BF%AE%E6%94%B9onapplicationinitialization">第三步</a>.</p>
<h4>HttpApi.Host项目</h4>
<p>按照下面修改<code>ConfigureSwaggerServices</code></p>
<pre><code class="language-diff">private void ConfigureSwaggerServices(IServiceCollection services, IConfiguration configuration)
{
+   Configure&lt;RapiDocOptions&gt;(options =&gt;
+   {
+       options.RoutePrefix = &quot;rapi-doc&quot;;//access url

+       // optional
+       options.GenericRapiConfig.RenderStyle = &quot;focused&quot;;// view | read | focused
+       options.GenericRapiConfig.Theme = &quot;dark&quot;; // light | dark
+       options.GenericRapiConfig.SchemaStyle = &quot;tree&quot;; // table | tree
+       options.GenericRapiConfig.ShowMethodInNavBar = &quot;as-colored-text&quot;; // false | as-plain-text | as-colored-text | as-colored-block
+       options.GenericRapiConfig.UsePathInNavBar = true;
+
+       // required
+       options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;RapiDocDemo API&quot;);
+       options.InjectJavascript(&quot;/swagger/ui/abp.js&quot;);
+       options.InjectJavascript(&quot;/rapi-doc/abp.rapi-doc.js&quot;);
+   });
    services.AddAbpSwaggerGenWithOAuth(
            authority: configuration[&quot;App:SelfUrl&quot;],
            scopes: new Dictionary&lt;string, string&gt;()
            {
                { &quot;RapiDocDemo&quot;, &quot;RapiDocDemo API&quot; }
            },
            options =&gt;
            {
                options.SwaggerDoc(&quot;v1&quot;, new OpenApiInfo { Title = &quot;RapiDocDemo API&quot;, Version = &quot;v1&quot; });
                options.DocInclusionPredicate((docName, description) =&gt; true);
                options.CustomSchemaIds(type =&gt; type.FullName);

+               // See https://rapidocweb.com/api.html#vendor-extensions
+               var oauth2SecurityScheme = options.SwaggerGeneratorOptions.SecuritySchemes.GetOrDefault+&quot;oauth2&quot;);
+               if (oauth2SecurityScheme != null)
+               {
+                   oauth2SecurityScheme.Extensions = new +ictionary&lt;string, Microsoft.OpenApi.Interfaces.IOpenApiExtension&gt;()
+                   {
+                       {&quot;x-client-id&quot;, new Microsoft.OpenApi.Any.OpenApiString(&quot;RapiDocDemo_RapiDoc&quot;)},
+                       {&quot;x-default-scopes&quot;, new Microsoft.OpenApi.Any.OpenApiString(&quot;RapiDocDemo&quot;)},
+                   };
+               }
            }
        );
}
</code></pre>
<p>DbMigrator项目<code>appsettings.json</code>进行如下修改.</p>
<pre><code class="language-diff">{
  ...
  &quot;OpenIddict&quot;: {
      ...
+     &quot;RapiDocDemo_RapiDoc&quot;: {
+       &quot;ClientId&quot;: &quot;RapiDocDemo_RapiDoc&quot;,
+       &quot;RootUrl&quot;: &quot;https://localhost:44394&quot; // your host
      }
    }
  }
}
</code></pre>
<p>按照下面配置<code>OpenIddictDataSeedContributor</code>的<code>CreateApplicationsAsync</code>方法.</p>
<pre><code class="language-diff">private async Task CreateApplicationsAsync()
{
    ...
+   var rapidocClientId = configurationSection[&quot;RapiDocDemo_RapiDoc:ClientId&quot;];
+   if (!rapidocClientId.IsNullOrWhiteSpace())
+   {
+       var rapidocRootUrl = configurationSection[&quot;RapiDocDemo_RapiDoc:RootUrl&quot;]?.TrimEnd('/');
+
+       await CreateApplicationAsync(
+           name: rapidocClientId!,
+           type: OpenIddictConstants.ClientTypes.Public,
+           consentType: OpenIddictConstants.ConsentTypes.Implicit,
+           displayName: &quot;RapiDoc Application&quot;,
+           secret: null,
+           grantTypes: new List&lt;string&gt;
+           {
+               OpenIddictConstants.GrantTypes.AuthorizationCode,
+           },
+           scopes: commonScopes,
+           redirectUri: $&quot;{rapidocRootUrl}/rapi-doc/oauth-receiver.html&quot;,
+           clientUri: rapidocRootUrl
+       );
+   }
}
</code></pre>
<p>运行DbMigrator项目.</p>
<h3>步骤3. 修改<code>OnApplicationInitialization</code></h3>
<pre><code class="language-diff">public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    ...
-   app.UseAbpSwaggerUI(options =&gt; { options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;Blogs API&quot;); });
+   //app.UseAbpSwaggerUI(options =&gt; { options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;Blogs API&quot;); });
+   app.UseRapiDocUI(options =&gt;
+   {
+       options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;Blogs API&quot;);
+       options.InjectJavascript(&quot;/swagger/ui/abp.js&quot;);
+       options.InjectJavascript(&quot;/swagger/ui/abp.rapi-doc.js&quot;);
+   });
    ...
}
</code></pre>
<p>如果你想同时使用swagger-ui和RapiDoc, 可以不用注释<code>app.UseAbpSwaggerUI</code>.</p>
<p>配置完成后运行项目, 访问<code>https://your-host/rapi-doc</code>开始使用.</p>
<h2>预览</h2>
<h3>RenderStyle focused</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-08-How-to-replace-swagger-ui-with-rapi-doc/media/preview1.png" alt="preview1" /></p>
<h3>OAuth authorization</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-08-How-to-replace-swagger-ui-with-rapi-doc/media/preview2.png" alt="preview2" /></p>
<h3>RenderStyle view</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-08-How-to-replace-swagger-ui-with-rapi-doc/media/preview3.png" alt="preview3" /></p>
<p>示例 https://github.com/JadynWong/RapiDocDemo</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-replace-swaggerui-with-rapidoc-hw7pktmz</guid>
      <link>https://abp.io/community/posts/how-to-replace-swaggerui-with-rapidoc-hw7pktmz</link>
      <a10:author>
        <a10:name>Jadyn</a10:name>
        <a10:uri>https://abp.io/community/members/Jadyn</a10:uri>
      </a10:author>
      <category>swagger</category>
      <category>RapiDoc</category>
      <title>How to replace SwaggerUI with RapiDoc</title>
      <description>The post shows how to replace SwaggerUI with RapiDoc as the default API documentation page in an abp.</description>
      <pubDate>Thu, 08 Jun 2023 10:35:25 Z</pubDate>
      <a10:updated>2026-10-05T14:51:37Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to replace SwaggerUI with RapiDoc</h1>
<h2>Introduction</h2>
<p>abp uses the very popular SwaggerUI as the default api documentation page. But if you get bored with its UI, you can try using <a href="https://rapidocweb.com/">RapiDoc</a> instead of Swagger-UI.</p>
<p>RapiDoc offers almost the same capabilities as SwaggerUI, but with a much nicer and more customizable UI.</p>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-08-How-to-replace-swagger-ui-with-rapi-doc/media/rapidoc.png" alt="RapiDoc" /></p>
<p>If you're interested, read on to learn how to use <a href="https://rapidocweb.com/">RapiDoc</a> as a documentation page for your project.</p>
<h2>Create a project</h2>
<p>Here I use a nolayers template for the demonstration, and the same steps for the other templates. You can also do the next steps on an existing project.</p>
<pre><code class="language-bash">abp new RapiDocDemo -t app-nolayers
</code></pre>
<h2>Install RapiDoc</h2>
<p>Thanks to <a href="https://github.com/luoyunchong">luoyunchong</a> for creating the <a href="https://github.com/luoyunchong/IGeekFan.AspNetCore.RapiDoc">IGeekFan.AspNetCore.RapiDoc</a> project, which makes it just as easy to use RapiDoc.</p>
<p>Let's add RapiDoc to the web project.</p>
<pre><code class="language-bash">dotnet add package IGeekFan.AspNetCore.RapiDoc
</code></pre>
<h2>Create <code>abp.rapi-doc.js</code> file</h2>
<p>Create the folder <code>wwwroot/rapi-doc</code> in the web project, and create the <code>abp.rapi-doc.js</code> file with the following content.</p>
<pre><code class="language-javascript">var abp = abp || {};

(function () {
    window.addEventListener('DOMContentLoaded', (event) =&gt; {
        var excludeUrl = [&quot;swagger.json&quot;, &quot;connect/token&quot;]
        var firstRequest = true;

        /*
          Ensure that the DOM is loaded, then add the event listener.
          here we are listenig to 'before-try' event which fires when the user clicks
          on TRY, it then modifies the POST requests by adding a custom header
        */
        const rapidocEl = document.getElementById('thedoc');
        rapidocEl.addEventListener('before-try', async (e) =&gt; {
            var request = e.detail.request;
            if (request.url.includes(excludeUrl[1])) {
                firstRequest = true;
            }

            if (firstRequest &amp;&amp; !excludeUrl.some(url =&gt; request.url.includes(url))) {
                await fetch(`${abp.appPath}abp/Swashbuckle/SetCsrfCookie`, {
                    headers: request.headers
                });
                firstRequest = false;
            }

            var antiForgeryToken = abp.security.antiForgery.getToken();
            if (antiForgeryToken) {
                request.headers.append(abp.security.antiForgery.tokenHeaderName, antiForgeryToken);
            }
        });
    });
})();
</code></pre>
<h2>Configure RapiDoc</h2>
<p>Configure <code>RapiDocDemoModule</code>.</p>
<h3>Step 1. Add using.</h3>
<pre><code class="language-csharp">using IGeekFan.AspNetCore.RapiDoc;
</code></pre>
<h3>Step 2. Configure <code>RapiDoc</code>.</h3>
<p>Configure <code>RapiDocOptions</code> by modify <code>ConfigureSwaggerServices</code> as follows.</p>
<pre><code class="language-diff">private void ConfigureSwaggerServices(IServiceCollection services, IConfiguration configuration)
{
+   Configure&lt;RapiDocOptions&gt;(options =&gt;
+   {
+       options.RoutePrefix = &quot;rapi-doc&quot;;//access url

+       // optional
+       options.GenericRapiConfig.RenderStyle = &quot;focused&quot;;// view | read | focused
+       options.GenericRapiConfig.Theme = &quot;dark&quot;; // light | dark
+       options.GenericRapiConfig.SchemaStyle = &quot;tree&quot;; // table | tree
+       options.GenericRapiConfig.ShowMethodInNavBar = &quot;as-colored-text&quot;; // false | as-plain-text | as-colored-text | as-colored-block
+       options.GenericRapiConfig.UsePathInNavBar = true;
+
+       // required
+       options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;RapiDocDemo API&quot;);
+       options.InjectJavascript(&quot;/swagger/ui/abp.js&quot;);
+       options.InjectJavascript(&quot;/rapi-doc/abp.rapi-doc.js&quot;);
+   });
    services.AddAbpSwaggerGen(
        options =&gt;
        {
            options.SwaggerDoc(&quot;v1&quot;, new OpenApiInfo { Title = &quot;Blogs API&quot;, Version = &quot;v1&quot; });
            options.DocInclusionPredicate((docName, description) =&gt; true);
            options.CustomSchemaIds(type =&gt; type.FullName);
        }
    );
}
</code></pre>
<blockquote>
<p>For more configurations, please refer to <a href="https://rapidocweb.com/api.html">https://rapidocweb.com/api.html</a>.</p>
</blockquote>
<p>For Web projects, see <a href="#step-3-modify-onapplicationinitialization">step 3</a> directly.</p>
<h4>HttpApi.Host project</h4>
<p>Configure <code>RapiDocOptions</code> as follows.</p>
<pre><code class="language-diff">private void ConfigureSwaggerServices(IServiceCollection services, IConfiguration configuration)
{
+   Configure&lt;RapiDocOptions&gt;(options =&gt;
+   {
+       options.RoutePrefix = &quot;rapi-doc&quot;;//access url

+       // optional
+       options.GenericRapiConfig.RenderStyle = &quot;focused&quot;;// view | read | focused
+       options.GenericRapiConfig.Theme = &quot;dark&quot;; // light | dark
+       options.GenericRapiConfig.SchemaStyle = &quot;tree&quot;; // table | tree
+       options.GenericRapiConfig.ShowMethodInNavBar = &quot;as-colored-text&quot;; // false | as-plain-text | as-colored-text | as-colored-block
+       options.GenericRapiConfig.UsePathInNavBar = true;
+
+       // required
+       options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;RapiDocDemo API&quot;);
+       options.InjectJavascript(&quot;/swagger/ui/abp.js&quot;);
+       options.InjectJavascript(&quot;/rapi-doc/abp.rapi-doc.js&quot;);
+   });
    services.AddAbpSwaggerGenWithOAuth(
            authority: configuration[&quot;App:SelfUrl&quot;],
            scopes: new Dictionary&lt;string, string&gt;()
            {
                { &quot;RapiDocDemo&quot;, &quot;RapiDocDemo API&quot; }
            },
            options =&gt;
            {
                options.SwaggerDoc(&quot;v1&quot;, new OpenApiInfo { Title = &quot;RapiDocDemo API&quot;, Version = &quot;v1&quot; });
                options.DocInclusionPredicate((docName, description) =&gt; true);
                options.CustomSchemaIds(type =&gt; type.FullName);

+               // See https://rapidocweb.com/api.html#vendor-extensions
+               var oauth2SecurityScheme = options.SwaggerGeneratorOptions.SecuritySchemes.GetOrDefault+&quot;oauth2&quot;);
+               if (oauth2SecurityScheme != null)
+               {
+                   oauth2SecurityScheme.Extensions = new +ictionary&lt;string, Microsoft.OpenApi.Interfaces.IOpenApiExtension&gt;()
+                   {
+                       {&quot;x-client-id&quot;, new Microsoft.OpenApi.Any.OpenApiString(&quot;RapiDocDemo_RapiDoc&quot;)},
+                       {&quot;x-default-scopes&quot;, new Microsoft.OpenApi.Any.OpenApiString(&quot;RapiDocDemo&quot;)},
+                   };
+               }
            }
        );
}
</code></pre>
<p>Modify the <code>appsettings.json</code> of the DbMigrator project as follows.</p>
<pre><code class="language-diff">{
  ...
  &quot;OpenIddict&quot;: {
      ...
+     &quot;RapiDocDemo_RapiDoc&quot;: {
+       &quot;ClientId&quot;: &quot;RapiDocDemo_RapiDoc&quot;,
+       &quot;RootUrl&quot;: &quot;https://localhost:44394&quot; // your host
      }
    }
  }
}
</code></pre>
<p>Configure the <code>CreateApplicationsAsync</code> method of the <code>OpenIddictDataSeedContributor</code> as follows.</p>
<pre><code class="language-diff">private async Task CreateApplicationsAsync()
{
    ...
+   var rapidocClientId = configurationSection[&quot;RapiDocDemo_RapiDoc:ClientId&quot;];
+   if (!rapidocClientId.IsNullOrWhiteSpace())
+   {
+       var rapidocRootUrl = configurationSection[&quot;RapiDocDemo_RapiDoc:RootUrl&quot;]?.TrimEnd('/');
+
+       await CreateApplicationAsync(
+           name: rapidocClientId!,
+           type: OpenIddictConstants.ClientTypes.Public,
+           consentType: OpenIddictConstants.ConsentTypes.Implicit,
+           displayName: &quot;RapiDoc Application&quot;,
+           secret: null,
+           grantTypes: new List&lt;string&gt;
+           {
+               OpenIddictConstants.GrantTypes.AuthorizationCode,
+           },
+           scopes: commonScopes,
+           redirectUri: $&quot;{rapidocRootUrl}/rapi-doc/oauth-receiver.html&quot;,
+           clientUri: rapidocRootUrl
+       );
+   }
}
</code></pre>
<p>Run DbMigrator project.</p>
<h3>Step 3. Modify <code>OnApplicationInitialization</code>.</h3>
<pre><code class="language-diff">public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    ...
-   app.UseAbpSwaggerUI(options =&gt; { options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;Blogs API&quot;); });
+   //app.UseAbpSwaggerUI(options =&gt; { options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;Blogs API&quot;); });
+   app.UseRapiDocUI(options =&gt;
+   {
+       options.SwaggerEndpoint(&quot;/swagger/v1/swagger.json&quot;, &quot;Blogs API&quot;);
+       options.InjectJavascript(&quot;/swagger/ui/abp.js&quot;);
+       options.InjectJavascript(&quot;/swagger/ui/abp.rapi-doc.js&quot;);
+   });
    ...
}
</code></pre>
<p>If you want to use swagger-ui and RapiDoc at the same time, you can leave out the <code>app.UseAbpSwaggerUI</code> comment.</p>
<p>Once configured, run the project, visit <code>https://your-host/rapi-doc</code> and start using it.</p>
<h2>Preview</h2>
<h3>RenderStyle focused</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-08-How-to-replace-swagger-ui-with-rapi-doc/media/preview1.png" alt="preview1" /></p>
<h3>OAuth authorization</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-08-How-to-replace-swagger-ui-with-rapi-doc/media/preview2.png" alt="preview2" /></p>
<h3>RenderStyle view</h3>
<p><img src="https://raw.githubusercontent.com/JadynWong/posts/main/2023-06-08-How-to-replace-swagger-ui-with-rapi-doc/media/preview3.png" alt="preview3" /></p>
<p>Example https://github.com/JadynWong/RapiDocDemo</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
  </channel>
</rss>