Current Path : /www/sites/www.coderblog.in/index/
Url:

NameSizeOptions
App_DataDIRnone
backupDIRnone
cgi-binDIRnone
cssDIRnone
imgDIRnone
metaDIRnone
wp-adminDIRnone
wp-contentDIRnone
wp-includesDIRnone
.htaccess3.35 KBDEL
.htaccess.bk1.84 KBDEL
404.html0.13 KBDEL
ads.txt1.07 KBDEL
favicon.ico2.19 KBDEL
index.php0.40 KBDEL
license.txt19.44 KBDEL
php.ini0.58 KBDEL
readme.html7.25 KBDEL
service.php0.00 KBDEL
web.config2.87 KBDEL
wp-activate.php7.21 KBDEL
wp-blog-header.php1.21 KBDEL
wp-comments-post.php2.27 KBDEL
wp-config-sample.php3.26 KBDEL
wp-config.php2.98 KBDEL
wp-cron.php5.49 KBDEL
wp-links-opml.php2.44 KBDEL
wp-load.php3.84 KBDEL
wp-login.php50.21 KBDEL
wp-mail.php8.52 KBDEL
wp-settings.php29.38 KBDEL
wp-signup.php33.71 KBDEL
wp-trackback.php4.98 KBDEL
xmlrpc.php3.13 KBDEL
Winson – Coder Blog https://www.coderblog.in Join the coding revolution! Learn, share, and grow together! Sat, 07 Mar 2026 02:51:28 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.1 Finally, a Static Site Generator That Speaks C#: Getting Started with Statiq https://www.coderblog.in/2026/03/finally-a-static-site-generator-that-speaks-c-getting-started-with-statiq/ https://www.coderblog.in/2026/03/finally-a-static-site-generator-that-speaks-c-getting-started-with-statiq/#comments Sun, 01 Mar 2026 04:01:47 +0000 https://www.coderblog.in/?p=1383 1. Introduction For years, the world of Static Site Generators (SSGs) has been dominated by ecosystems outside of

<p>The post Finally, a Static Site Generator That Speaks C#: Getting Started with Statiq first appeared on Coder Blog.</p>

]]>
1. Introduction

For years, the world of Static Site Generators (SSGs) has been dominated by ecosystems outside of .NET. We’ve watched from the sidelines as JavaScript developers enjoyed Gatsby and Next.js, and Go developers embraced Hugo. For .NET developers, the options were often limited, outdated, or required leaving our comfortable C# environment to wrestle with unfamiliar build chains.

Enter Statiq.

Statiq isn’t just another static site generator; it is a powerful, flexible framework designed specifically for the .NET ecosystem. It allows you to use the languages, libraries, and patterns you already know and love — Razor, Markdown, and C# — to build lightning-fast static websites. Whether you are building a simple blog, a documentation site, or a complex enterprise web application, Statiq brings the robustness of .NET to the JAMstack.

In this guide, I’ll walk you through exactly what Statiq is, why it’s a game-changer for .NET developers, and how you can get your first site up and running in minutes.

At the end, I will provide a full site and theme with full features below to you!

  1. Search function
  2. Use tags in the site like a blog system
  3. Add the Google AdSense and Analytics service
  4. Archives page
  5. Listing page for the category

2. Why do we need to generate the static website?

Maybe you will wonder why we need to do that? We can use the WordPress to create a beautiful site easily. Yes, you can! But for hosting a WordPress website, you need to handle the following issues:

  1. A host can support PHP (I think maybe this is not an issue)
  2. Performance issue (with a log of plugins )
  3. Security issue. All dynamic websites are at risk of being hacked because they may have some unknown vulnerabilities or you may not update them in time. (I’ve had my WP site hacked before 🥲)

Conversely, however, you can find the following benefits of using a static website

  1. You can use a very cheap hosting only support the HTML
  2. You can get very good performance
  3. Your site will be very secure, because the static site is completely without vulnerabilities, if your host only supports HTML, then the other side will be difficult to inject and other attacks (This is what I like 😀)
  4. You can easily convert any HTML template to your theme
  5. The Pure HTML static websites are very SEO friendly

3. What’s Statiq?

At its core, Statiq is a powerful, flexible static generation framework built on the modern .NET platform. If you are coming from the world of JavaScript (Gatsby, Next.js) or Go (Hugo), you can think of Statiq as the .NET equivalent, but with a significant architectural twist.

While most Static Site Generators (SSGs) are opinionated “tools” configured via YAML or TOML files, Statiq is designed as a framework. This means you don’t just configure your site; you program it.

The Philosophy: Pipelines and Modules

Statiq operates on a unique concept of documentspipelines, and modules:

  • Everything is a Document: In Statiq, a “document” isn’t just a Markdown file. It can be an image, a data file, a Razor template, or even an API response.
  • Pipelines: You define pipelines to process these documents. A pipeline is simply a sequence of steps.
  • Modules: These represent the individual steps within a pipeline. For example, you might have a module to ReadFiles, another to RenderMarkdown, and a final one to WriteFiles.

This architecture gives you granular control over how your content is fetched, transformed, and generated.

Why Choose Statiq?

  1. The .NET Ecosystem: You can leverage the full power of C# and NuGet packages. If you need to fetch data from a database or call a custom API during the build process, you can simply write C# code to do it.
  2. Razor & Markdown: It has first-class support for Razor (the view engine used in ASP.NET Core) and Markdown, allowing you to build complex layouts with logic while writing content in simple text.
  3. Statiq.Web: While the core framework is generic, Statiq.Web is a specialized library built on top of it. It comes pre-configured with common pipelines for assets, content, and templates, allowing you to get a standard website up and running with minimal code.

The workflow in Statiq

Statiq Workflow

In short, Statiq bridges the gap between the simplicity of a static site and the dynamic capabilities of a .NET application.

4. Create and support multiple themes

Statiq is very easy to use, you can follow the office guide to create your first app here. And I will show you more details on how to create your own theme and a complete website with Statiq! 😁

4.1 Multiple themes

By default, Statiq guide only provides the simple and single theme structure below

But I want to support multiple themes and can easily use them in appsettings.json

To support multiple themes, we need to create a themes folder, and create a theme folder, put the template pages into the theme’s input folder (actually, this is not necessary; you can also put the pages into the theme root folder).

Create the appsettings.json in project root with below content

{
    "Theme": "themes/editorial"
}

Read the config in Program.cs when creating the Boststrapper

return await Bootstrapper
              .Factory
              .CreateWeb(args)
              .ConfigureSettings(settings =>
              {
                  if (!settings.ContainsKey("Theme"))
                  {
                      settings["Theme"] = "themes/default";
                  }
              })

And handle the theme template files in ConfigureFileSystem in Program.cs

.ConfigureFileSystem((fileSystem, settings) =>
 {
     var themePath = settings.GetString("Theme");
     if (!string.IsNullOrEmpty(themePath))
     {
         // Create a validated path from the setting
         var themePathNormalized = new NormalizedPath(themePath);

         // Ensure the path is absolute. If it's relative, combine it with the root path.
         if (themePathNormalized.IsRelative)
         {
             themePathNormalized = fileSystem.RootPath.Combine(themePathNormalized);
         }

         // Check if the theme has an 'input' folder (common convention)
         // If it does, strictly use that as the input path instead of the theme root
         var themeInputPath = themePathNormalized.Combine("input");

         if (fileSystem.GetDirectory(themeInputPath).Exists)
         {
             fileSystem.InputPaths.Add(themeInputPath);
         }
         else
         {
             fileSystem.InputPaths.Add(themePathNormalized);
         }
     }
 })

That’s it, then we can switch the theme any time.

4.2 Create the theme layout

Ok, next, I will base on a free HTML template of the HTML 5UP to create a Statiq theme.

We can to take a look at the HTML template first

as this layout, we should create some pages as below

  1. _Layout.cshtml : The main structure
  2. Index.cshtml: The home page
  3. _Sidebar.cshtml: The sidebar

These are the main pages, but we need more, like archives, tags, search, and 404 pages…

So the theme folder structure should be like below

I already created a website Tableware.com that is based on Statiq.

So I will use this website to explain how to create the theme.

First, we define the main layout structure in _Layout.cshtml

<!-- Wrapper -->
    <div id="wrapper">

        <!-- Main -->
        <div id="main">
            <div class="inner">

                <!-- Header -->
                <header id="header">
                    <a href="/" class="logo"><strong>Tableware</strong></a>
                    @* <ul class="icons">
                        <li><a href="#" class="icon brands fa-twitter"><span class="label">Twitter</span></a></li>
                        <li><a href="#" class="icon brands fa-facebook-f"><span class="label">Facebook</span></a></li>
                        <li><a href="#" class="icon brands fa-instagram"><span class="label">Instagram</span></a></li>
                    </ul> *@
                </header>
                @RenderBody()
            </div>
        </div>

        <!-- Sidebar -->
        @await Html.PartialAsync("_Sidebar")

    </div>

and create the sidebar, because there are too many codes, so I will only show the core logic on how to get the posts in views, but don’t worry, I will provide all source code at the end:

<div class="mini-posts">
                @{
                    // 1. Get all posts in the "posts" folder
                    // 2. Filter out posts without a date
                    // 3. Order by date in descending order
                    // 4. x.ContainsKey("Date"): only find the post with "Date" metadata,
                    // exclude pages like "About Us" without date
                    // 5. !x.Source.IsNull: exclude pages like search-index.json
                    // 6. Make sure the source contain "posts"
                    var recentPosts = OutputPages
                        .Where(x => x.ContainsKey("Date") && !x.Source.IsNull && x.Source.ToString().Contains("posts"))
                        .OrderByDescending(x => x.Get<DateTime>("Date"))
                        .Take(3);
                }

                @if (recentPosts.Any())
                {
                    foreach (var doc in recentPosts)
                    {
                        <article>
                            @if (doc.ContainsKey("Image"))
                            {
                                <a href="@doc.GetLink()" class="image">
                                    <img src="@doc.GetString("Image")" alt="@doc.GetString("Title")" />
                                </a>
                            }

                            <h3 style="font-size: 1em;">
                                <a href="@doc.GetLink()">@doc.GetString("Title")</a>
                            </h3>

                            <p style="font-size: 0.8em; margin-bottom: 0;">
                                @(doc.Get<DateTime>("Date").ToLongDateString())
                            </p>
                        </article>
                    }
                }
                else
                {
                    <p>No updates yet.</p>
                }
            </div>
            <ul class="actions">
                <li><a href="/archives" class="button">More Archives</a></li>
            </ul>
        </section>

You can get the post from input/post folder, suppose the posts are created with markdown format

and the code snippet on the home page ( index.cshtml) for showing how to get posts to display

<div class="posts">
            @{
                // 1. Scan all output pages
                // 2. Filter condition: source file not null + source file path contains "posts" + must be .md file (exclude index.cshtml)
                // 3. Sort: use Get<DateTime> to sort by date in descending order
                var recentPosts = OutputPages
                    .Where(x => x.Source != null
                             && x.Source.ToString().Contains("posts")
                             && x.Source.Extension == ".md"
                             && x.ContainsKey("Date"))
                    .OrderByDescending(x => x.Get<DateTime>("Date"))
                    .Take(6);
            }

            @foreach (var doc in recentPosts)
            {
                <article>
                    <a href="@doc.GetLink()" class="image">
                        <img src="@(doc.GetString("Image") ?? "/images/pic01.jpg")" alt="@doc.GetTitle()" />
                    </a>
                    <h3>@doc.GetTitle()</h3>

                    <p>@doc.GetString("Description")</p>

                    <ul class="actions">
                        <li><a href="@doc.GetLink()" class="button">Read Review</a></li>
                    </ul>
                </article>
            }
        </div>

5. Core Features

I will show you how to create the core features for a static website in Statiq.

5.1 Search Function

The search function is a necessary function for a website, but our website is only a static HTML site, so how can we do it?

We can use Lunr.js !

Lunr.js is a lightweight, full-text search library designed specifically for client-side use. Unlike traditional search engines (like Elasticsearch or Solr) that require a server to process queries, Lunr runs entirely in the user’s browser.

It works by taking a JSON dataset, building an inverted index in memory, and then performing searches against that index. This makes it a perfect companion for static sites (Jamstack) because it requires no external dependencies, no server-side code, and no API keys. It’s small, fast, and handles fuzzy matching and boosting, providing a “real” search experience on a static HTML page.

Implementation:

Step 1: Generate the Search Index (The Statiq Side)

We need to generate all content into a JSON file. Add the below code in program.cs in ConfigureEngine

.ConfigureEngine(engine =>
                {
                    // Get the default "Content" pipeline
                    // This is the pipeline where Statiq Web processes all Markdown and Razor files
                    var contentPipeline = engine.Pipelines["Content"];

                    // Add a module in the "PostProcess" phase (after all processing is done, but before writing to disk)
                    contentPipeline.PostProcessModules.Add(
                    new SetDestination(Config.FromDocument(doc =>
                    {
                        // 1. Get the source file name
                        var sourceName = doc.Source.FileName.ToString();

                        // 2. Check: If it is our search index file
                        if (sourceName.Contains("search-index.json"))
                        {
                            // [Critical Fix] Use NormalizedPath instead of FilePath
                            return new NormalizedPath("search-index.json");
                        }

                        // 3. Keep other files as they are
                        return doc.Destination;
                    }))
                    );
...
...

And then create a Razor view (e.g., search-index.json.cshtml). This file iterates through all the published documents in the Statiq pipeline.

It will output a single search-index.json file containing the titledescriptionurl, and content (or keywords) for every page.

This effectively turns your site content into a portable database. The below complete code in search-index.json.cshtml

---
Permalink: /search-index.json
---
@using System.Text.Json
@using System.Text.RegularExpressions
@using Statiq.Common
@using System.Net 

@{
    Layout = null;

    // Define the clean function
    Func<string, string> CleanText = (input) =>
    {
        if (string.IsNullOrEmpty(input)) return string.Empty;

        // 1. Remove the HTML tags (like <p>, <div>, etc.)
        var text = Regex.Replace(input, "<.*?>", " ");

        // 2. Decode "   HTML tags
        text = WebUtility.HtmlDecode(text);

        // 3. Remove newlines, tabs, and collapse multiple spaces into one
        text = Regex.Replace(text, @"\s+", " ").Trim();

        return text;
    };

    var searchEntries = new List<object>();

    foreach (var doc in OutputPages)
    {

        // 1. Only index HTML files (by extension)
        if (doc.Destination.Extension != ".html") continue;

        // 2. Must have a title
        if (!doc.ContainsKey("Title") || string.IsNullOrEmpty(doc.GetString("Title"))) continue;

        // 3. Get the file name
        var fileName = doc.Destination.FileName.ToString();

        // 4. Exclude the search page, index page, and the search index itself
        if (fileName.Equals("search.html", StringComparison.OrdinalIgnoreCase)) continue;
        if (fileName.Equals("index.html", StringComparison.OrdinalIgnoreCase)) continue;
        if (fileName.Contains("search-index")) continue; 

        // --- Handle content ---

        // Synchronously get the content (since we are in a synchronous context)
        string rawContent = doc.GetContentStringAsync().GetAwaiter().GetResult();

        // Clean the content
        string cleanContent = CleanText(rawContent);

        // Get the 5000 first characters of the content (or less if the content is shorter)
        if (cleanContent.Length > 5000)
        {
            cleanContent = cleanContent.Substring(0, 5000);
        }

        searchEntries.Add(new
        {
            title = doc.GetString("Title"),
            link = doc.GetLink(),
            description = doc.GetString("Description") ?? "",
            content = cleanContent
        });
    }

    var options = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = false
    };

    string json = JsonSerializer.Serialize(searchEntries, options);
}
@Html.Raw(json)

Step 2: Load the Index (The Client Side)

When the page loads, a simple JavaScript function uses fetch() to retrieve the search-index.json file we generated in Step 1.

Once the JSON is loaded, we pass it to Lunr. Lunr analyzes the text, removes stop words (like “the”, “and”), stems the words (converting “running” to “run”), and builds its internal index for fast retrieval.

We need to create a search.cshtml for that

Title: Search Results
Layout: _Layout.cshtml
---
<section>
    <header class="main">
        <h1>Search Results</h1>
    </header>
    
    <div id="search-results-container">
        <p>Searching...</p>
    </div>
</section>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lunr.js/2.3.9/lunr.min.js"></script>
<script>
    document.addEventListener("DOMContentLoaded", function() {
        const params = new URLSearchParams(window.location.search);
        const query = params.get("query");
        const container = document.getElementById("search-results-container");
        
        if (!query) {
            container.innerHTML = "<p>Please enter a search term.</p>";
            return;
        }

        fetch("/search-index.json")
            .then(response => response.json())
            .then(data => {
                const idx = lunr(function () {
                    this.ref('link');
                    this.field('title');
                    this.field('description');
                    this.field('content');

                    data.forEach(function (doc) {
                        this.add(doc);
                    }, this);
                });

                const results = idx.search(query);
                
                if (results.length === 0) {
                    container.innerHTML = "<p>No results found for '" + query + "'.</p>";
                } else {
                    let html = '<div class="posts">';
                    results.forEach(result => {
                        const item = data.find(d => d.link === result.ref);
                        html += `
                            <article>
                                <h3>${item.title}</h3>
                                <p>${item.description || ''}</p>
                                <ul class="actions">
                                    <li><a href="${item.link}" class="button">Read More</a></li>
                                </ul>
                            </article>
                        `;
                    });
                    html += "</div>";
                    container.innerHTML = html;
                }
            })
            .catch(error => {
                console.error("Error:", error);
                container.innerHTML = "<p>An error occurred.</p>";
            });
    });
</script>

Step 3: Execute the Search

Ok, we have a simple HTML search form in the sidebar for this theme, when you submit the search query, Lunr queries the in-memory index and returns a list of matching document references (specifically the ref ID, which maps back to our URL).

 <!-- Search -->
        <section id="search" class="alt">
            <form method="get" action="/search">
                <input type="text" name="query" id="query" placeholder="Search" />
            </form>
        </section>

Limitations and Scalability: When to Switch?

While the Statiq + Lunr.js approach is powerful, cost-effective, and privacy-friendly, it relies entirely on the client’s browser. It is not a “one-size-fits-all” solution. Here are the realistic limits you should consider:

A. The “Payload” Issue (File Size) Since the entire search index (search-index.json) must be downloaded by the user, the file size grows linearly with your content.

  • Full-Text Indexing: If you index the entire body of every article, the file can become heavy quickly.
  • Optimization: To mitigate this, I recommend indexing only the TitleTags, and a short Excerpt/Description. This keeps the file lightweight even as your blog grows.

B. Browser Performance Lunr needs to parse the JSON and build the inverted index in memory. For a site with thousands of pages, this process can block the main thread, causing the UI to freeze momentarily on slower devices (especially mobile phones).

C. The Verdict: How many pages?

  • Small to Medium Sites (< 1,000 pages): This solution is excellent. The performance impact is negligible.
  • Large Sites (> 2,000 pages): You might start hitting performance bottlenecks. At this scale, the index file size becomes a burden.
  • Enterprise Scale: If you have tens of thousands of documents, you should look into server-side solutions like AlgoliaElasticsearch, or Azure AI Search.

5.2 The Tags Function

If you would like to create a blog site, I think you need a Tags function, you can add the tags for each post, ok, let’s do it!

We need to add a pipeline for handling the tags logic in Program.cs

// We create a new pipeline named "Tags"
engine.Pipelines.Add("Tags", new Pipeline
{
    // Depends on the Content pipeline, ensuring articles have been read
    Dependencies = { "Content" },
    ProcessModules = {
        // A. Read all articles processed by the "Content" pipeline
        new ReplaceDocuments("Content"),
        // B. Filter out articles without Tags to prevent errors
        new FilterDocuments(Config.FromDocument(doc => doc.ContainsKey("Tags"))),
        // C. Core: Group by the "Tags" field
        // This turns 100 articles into (for example) 10 documents, where each document represents a Tag
        new GroupDocuments("Tags"),
        // D. Load the template we just wrote
        new MergeContent(new ReadFiles("_TagLayout.cshtml")),
        // E. Render Razor
        new RenderRazor()
            .WithModel(Config.FromDocument((doc, ctx) => doc)),
        // F. Set output path: /tag/tag-name.html
        new SetDestination(Config.FromDocument(doc =>
        {
            var tagName = doc.GetString(Keys.GroupKey);
            var slug = tagName.ToLower().Replace(" ", "-");
            return new NormalizedPath($"tag/{slug}.html");
        })),
        // [Critical Fix] G. Write files!
        // This step is mandatory, otherwise the files only exist in memory
        new WriteFiles()
    }
});

Create the _TagLayout.cshtml

@{
    Layout = "_Layout.cshtml";
    // Get the current tags name
    var tagName = Document.GetString(Keys.GroupKey);
}

<section>
    <header class="main">
        <h1>Tag: <em>@tagName</em></h1>
    </header>

    <div class="posts">
        @foreach (var post in Document.GetChildren())
        {
            <article>
                @if (post.ContainsKey("Image"))
                {
                    <a href="@post.GetLink()" class="image">
                        <img src="@post.GetString("Image")" alt="" />
                    </a>
                }
                <h3><a href="@post.GetLink()">@post.GetString("Title")</a></h3>
                <p>@post.GetString("Description")</p>
                <ul class="actions">
                    <li><a href="@post.GetLink()" class="button">Read More</a></li>
                </ul>
            </article>
        }
    </div>
</section>

The result is below

And we also can do more, in order to improve SEO, we can automatically generate internal links for articles based on the tags.

Create a ParallelModule in TagAutoLinkModule.cs in project root

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Statiq.Common;

public class TagAutoLinkModule : ParallelModule
{
   protected override async Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context)
    {
        // 1. Retrieve all tags and their corresponding URLs from the entire context
        // Assuming your tag page path format is /tags/tag-name
        var allTags = context.Outputs
        .SelectMany(doc => doc.GetList<string>("Tags") ?? Enumerable.Empty<string>()) // Directly use the string "Tags"
        .Distinct()
        .OrderByDescending(t => t.Length)
        .ToDictionary(
            tag => tag,
            tag => $"/tag/{tag.ToLower().Replace(" ", "-")}"
        );

        string content = await input.GetContentStringAsync();

        // 2. Iterate through all tags to process the current article
        foreach (var tag in allTags)
        {
            // Prevent in-article tags from linking to themselves (optional)
            // string pattern = $@"(?<!<[^>]*){tag}(?![^<]*</a>)";

            // Complex Regex: Match tags, but exclude cases where they are already inside <a> tags or HTML attributes
            string pattern = $@"\b({Regex.Escape(tag.Key)})\b(?![^<]*>)(?![^<]*</a>)";

            content = Regex.Replace(content, pattern, $"<a href=\"{tag.Value}\" class=\"internal-tag-link\">$1</a>", RegexOptions.IgnoreCase);
        }

        return input.Clone(context.GetContentProvider(content, "text/html")).Yield();
    }
}

It will find the keywords to match the tags and add the hyperlinks. Process the module in pipline

ModifyPipeline(nameof(Statiq.Web.Pipelines.Content), pipeline =>
{
    // Add custom module to PostModules
    // This ensures it runs after all standard processing (like Markdown rendering) is complete
    pipeline.ProcessModules.Add(new TagAutoLinkModule());
})

And you can find the result below

Auto add the hyperlinks to match the Tags

5.3 Pagination function

For a static website, the pagination is also an issue, but Statiq can help to handle it.

For example, we have a listing page template to show all of the posts in a category in _ListLayout.cshtml

We need to get the posts from input folder

@{
    // 1. Define the variable IEnumerable<IDocument>
    IEnumerable<IDocument> posts;
    // 2. Get the children docs
    var childDocs = Document.GetChildren();
    if (childDocs.Any())
    {
        posts = childDocs;
    }
    else
    {
        posts = OutputPages.GetChildrenOf(Document)
            .Where(x => x.Source.Extension == ".md" && x.Id != Document.Id)
            .OrderByDescending(x => x.Get<DateTime>("Date", DateTime.MinValue));
    }
}
@foreach (var post in posts)
{
    <article>
        @if (post.ContainsKey("Image"))
        {
            <a href="@post.GetLink()" class="image">
                <img src="@post.GetString("Image")" alt="" />
            </a>
        }
        <h3><a href="@post.GetLink()">@post.GetString("Title")</a></h3>
        <p>@post.GetString("Description")</p>
        <ul class="actions">
            <li><a href="@post.GetLink()" class="button">Read More</a></li>
        </ul>
    </article>
}

and handle the pagination logic

@{
    // Get the previouse and next page documents
    var prevPage = Document.GetDocument(Keys.Previous);
    var nextPage = Document.GetDocument(Keys.Next);
    // Get the total pages and current page number
    var totalPages = Document.GetInt(Keys.TotalPages);
    var currentPage = Document.GetInt(Keys.Index);
}
@if (totalPages > 1)
{
    <div class="pagination" style="margin-top: 3rem; text-align: center;">
        @if (prevPage != null)
        {
            <a href="@prevPage.GetLink()" class="button">Prev</a>
        }
        else
        {
            <span class="button disabled">Prev</span>
        }
        <span class="extra" style="margin: 0 1rem;">
            Page @currentPage of @totalPages
        </span>
        @if (nextPage != null)
        {
            <a href="@nextPage.GetLink()" class="button">Next</a>
        }
        else
        {
            <span class="button disabled">Next</span>
        }
    </div>
}

5.4 Archives

The last feature is showing the archives. This is also a common feature for the blog site.

Actually, we just need to get the posts by groping and order then we can get the archives data.

var allPosts = OutputPages
.Where(x => x.Source != null
&& x.Source.ToString().Contains("posts")
&& x.Source.Extension == ".md"
                 && x.ContainsKey("Date"));
var postsByYear = allPosts
.OrderByDescending(x => x.Get<DateTime>("Date"))
.GroupBy(x => x.Get<DateTime>("Date").Year);

Create an archives.cshtml templage to display the data

foreach (var yearGroup in postsByYear)
 {
     <h2 id="year-@yearGroup.Key" style="border-bottom: 2px solid #f56a6a; padding-bottom: 10px; margin-top: 2em;">
         @yearGroup.Key
     </h2>

     <div class="table-wrapper">
         <table>
             <thead>
                 <tr>
                     <th style="width: 120px;">Date</th>
                     <th>Title</th>
                     <th>Category / Tags</th>
                 </tr>
             </thead>
             <tbody>
                 @foreach (var post in yearGroup)
                 {
                     var postDate = post.Get<DateTime>("Date");

                     <tr>
                         <td style="white-space: nowrap; width: 120px;">
                             @postDate.ToString("MMM dd")
                         </td>

                         <td>
                             <a href="@post.GetLink()" style="font-weight: bold;">
                                 @post.GetString("Title")
                             </a>
                         </td>

                         <td>
                             @if (post.ContainsKey("Tags"))
                             {
                                 foreach (var tag in post.GetList<string>("Tags").Take(3))
                                 {
                                     <span class="button small"
                                           style="font-size: 0.6em; height: 2em; line-height: 2em; padding: 0 10px; margin-right: 2px;">
                                         @tag
                                     </span>
                                 }
                             }
                         </td>
                     </tr>
                 }
             </tbody>
         </table>
     </div>
 }

6. Create the content

After creating the theme and feature, we can start to create our content. Statiq will read the content from the input folder, so we must put the content in it. And we can use markdown to create our content.

For example in my website, I put all article in input/posts folder and put the image in input/images folder, and create a post below

---
Title: "The 10 Best Dinnerware Sets of 2026: Trends, Reviews & Buying Guide"
Description: "We tested the top dinnerware sets of 2026. From durable Corelle to luxurious Wedgwood bone china, here are the best plates for every budget and style."
Date: 2026-02-08
Layout: "_PostLayout"
Image: "/images/hero-best-dinnerware-2026.webp"
Tags: [Buying Guide, 2026 Trends, Bone China, Stoneware, Luxury]
---

Other content below ......

You need to set the metdata in the top of the file with markdown format, then the theme will read them.

7. Deployment

Every time you change the content or theme, Statiq will generate all HTML files, so if you just upload all or the updated file to FTP then it will also be an issue.

If your hosting provide SSH and Linux OS, then the below deployment will be perfect for you!

You can use rsync to upload only changed files to your server, and you don’t need to select one by one or check which one should be uploaded

If you are use Mac OS, then the rsync is a built-in command, if you are using Windows, then you need to install the Git Bash or WSL

To check whether your hosting can be support rsync , you can use below commmand

ssh username@your-server.com "rsync --version"

Ok, you can use the script below to deploy your HTML site to the server

#!/bin/bash

# 1. the ssh key 
KEY_PATH="~/.ssh/ssh.key"

# 2. VPS Information
USER="root"      
IP="VPS IP"     
REMOTE_DIR="/opt/www/sites/yourdomain.com/" 

echo "Generating static website..."
dotnet run

echo "Syncing output to server..."

rsync -avz --delete --rsync-path="sudo rsync" -e "ssh -i $KEY_PATH" output/ ${USER}@${IP}:${REMOTE_DIR}

echo "Deployment complete!"

That’s it, so simple! 😁

The full project and theme for you

You can find my full project below

GitHub – coderblog-winson/statiq_theme: A theme for Statiq

A theme for Statiq. Contribute to coderblog-winson/statiq_theme development by creating an account on GitHub.

github.com

8. Conclusion

For me, discovering Statiq was a game-changer. It proved that I didn’t need to learn a new JavaScript framework just to build a fast, secure website. The combination of Statiq’s robust generation pipeline and the lightweight simplicity of Lunr.js created the perfect architecture for my needs.

Whether you are building a portfolio, a company blog, or a documentation site, this stack offers incredible speed and flexibility. I encourage you to clone the repo, tweak the pipelines, and build something of your own. The .NET static site community is growing, and now is the perfect time to jump in.

Loading

<p>The post Finally, a Static Site Generator That Speaks C#: Getting Started with Statiq first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2026/03/finally-a-static-site-generator-that-speaks-c-getting-started-with-statiq/feed/ 3
OpenCode: An Open-Source Alternative to Claude Code https://www.coderblog.in/2026/02/opencode-an-open-source-alternative-to-claude-code/ https://www.coderblog.in/2026/02/opencode-an-open-source-alternative-to-claude-code/#comments Sat, 28 Feb 2026 09:26:22 +0000 https://www.coderblog.in/?p=1376 1. Introduction It’s hard to ignore how much attention Claude Code has been getting lately. Its capabilities are impressive, and

<p>The post OpenCode: An Open-Source Alternative to Claude Code first appeared on Coder Blog.</p>

]]>
1. Introduction

It’s hard to ignore how much attention Claude Code has been getting lately. Its capabilities are impressive, and for many developers, it has become a powerful companion that makes everyday coding tasks significantly easier.

That said, as polished and effective as Claude Code is, it remains a paid product — and not a particularly inexpensive one. This naturally raises a question: is there an alternative that offers similar power without the same constraints?

That’s where OpenCode comes in.

OpenCode is an open-source, AI-assisted coding tool that can comfortably stand alongside Claude Code in terms of functionality. In practice, most of what Claude Code can do, OpenCode can do as well. It even provides access to free models out of the box. More importantly, OpenCode gives developers full control: you can configure your own models, choose your own providers, and shape the workflow to fit your preferences rather than the other way around.

In the following sections, I’ll take a closer look at OpenCode — what it offers, how it feels to use, and why it’s worth paying attention to.

2. The OpenCode

The OpenCode supports multiple platforms, you can download it here.

It supports IDE and CLI modes, if you are Cladue Code user, you can try the CLI mode, which can be supported well in VS Code, the screen like below

OpenCoode with VS CODE

and the IDE mode as below

3. Trying the Vibe Coding

OK, let’s try it for how it’s working in vibe coding.

For the demo, I just ask it to create a simple blog website with ASP.NET Core, it will generate the to-do list and do it step by step

The first time, it will build all of the code of the website, but there is an error below

SqliteException: SQLite Error 1: 'no such table: Blogs'.

because didn’t do the database migration, and I keep to ask it to fix

After that, the website can be run very well 🙂

Of course, this is only a very simple demo, but we can see that it can work very well.

4. Why OpenCode

I think the advantages of OpenCode are the cost, you can free to use a few models, and it can let you know how many tokens are used in this section

and also can see that in IDE mode

And I am very like it can set the custom API and models.

OpenCode supports many of AI providers

But if you still can’t find the provider that you want, don’t worry, you can add it by yourself.

Update or create the opencode.json file in your project folder or below

Remote config (from .well-known/opencode) - organizational defaults
Global config (~/.config/opencode/opencode.json) - user preferences
Custom config (OPENCODE_CONFIG env var) - custom overrides
Project config (opencode.json in project) - project-specific settings
.opencode directories - agents, commands, plugins
Inline config (OPENCODE_CONFIG_CONTENT env var) - runtime overrides

for example, I want to add a new provider and update the opencode.jsonas below

{
    "$schema": "https://opencode.ai/config.json",
    "provider": {
        "chatfire": {
            "npm": "@ai-sdk/openai-compatible",
            "name": "ChatFire API",
            "options": {
                "baseURL": "https://api.chatfire.cn/v1"
            },
            "models": {
                "gemini-3-flash-preview-thinking": {
                    "name": "gemini-3-flash-preview-thinking"
                },
                "gpt-4o-mini": {
                    "name": "gpt-4o-mini"
                }
            }
        }
    }
}

and also update the ./local/share/opencode/auth.json to set the API key

{
  "chatfire": {
    "type": "api",
    "key": "your api key"
  }
}

and then I will find this provider

also can connect the provider in CLI mode

and you will can use the new models from the custom provider

also same in CLI mode

This is what I want, how about you? 😄

5. Conclusion

OpenCode is open source and provides free models AI-assisted coding tool, and it can be working well with VS Code, and also can setup the custom API provider, so if you think the Claude Code is too expensive or want to try another one, you can try OpenCode 😁

Loading

<p>The post OpenCode: An Open-Source Alternative to Claude Code first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2026/02/opencode-an-open-source-alternative-to-claude-code/feed/ 3
Goodbye Verbose Code: 5 Modern C# Features That Changed My Workflow https://www.coderblog.in/2025/11/goodbye-verbose-code-5-modern-c-features-that-changed-my-workflow/ https://www.coderblog.in/2025/11/goodbye-verbose-code-5-modern-c-features-that-changed-my-workflow/#comments Sat, 22 Nov 2025 14:10:06 +0000 https://www.coderblog.in/?p=1366 I’ve been writing C# for a long time. I remember the days of C# 2.0 and 3.0, where

<p>The post Goodbye Verbose Code: 5 Modern C# Features That Changed My Workflow first appeared on Coder Blog.</p>

]]>
I’ve been writing C# for a long time. I remember the days of C# 2.0 and 3.0, where explicit types were mandatory, asynchronous programming involved complex callback hell, and creating a simple data object required twenty lines of boilerplate code.

For years, C# (much like Java) had a reputation for being verbose. We spent more time typing structural boilerplate than actual business logic.

But things have changed. With the rapid release cadence of .NET Core (now just .NET) and C# versions 8 through 12, the language has transformed. It has become more expressive, functional, and concise.

As a senior developer, I don’t adopt new syntax just because it’s “shiny.” I adopt it if it reduces cognitive load and makes code easier to read. Here are the 5 modern C# features that have genuinely changed my daily workflow.

1. File-Scoped Namespaces (C# 10)

This might seem like a trivial cosmetic change, but it is the single most satisfying cleanup feature for me.

The Old Way: In the past, every file started with a namespace indentation tax. Your entire class was shifted four spaces to the right, wasting horizontal screen real estate.

namespace MyApp.Services
{
    public class UserService
    {
        public void DoSomething()
        {
            // Logic here...
        }
    }
}

The New Way: With File-Scoped Namespaces, you declare the namespace once at the top, end it with a semicolon, and remove the curly braces.

namespace MyApp.Services;

public class UserService
{
    public void DoSomething()
    {
        // Logic here...
    }
}

Why it changed my workflow: It removes an unnecessary level of nesting. When I open a file, the code is flush with the left margin. It looks cleaner, reads better on smaller laptop screens, and reduces the “pyramid of doom” effect when you have nested logic inside methods.

2. Records & Positional Syntax (C# 9)

I write a lot of DTOs (Data Transfer Objects) and API response models. In “Old C#,” creating an immutable object that supported value-based equality was a nightmare of boilerplate. You had to override EqualsGetHashCode, and create a constructor to set properties.

The Old Way:

public class CustomerDto
{
    public string Name { get; }
    public string Email { get; }

    public CustomerDto(string name, string email)
    {
        Name = name;
        Email = email;
    }

    // Imagine 20 more lines of Equals/GetHashCode overrides here...
}

The New Way: Enter record.

public record CustomerDto(string Name, string Email);

That’s it. That is the entire file.

Why it changed my workflow: This turns a 30-line file into a 1-line definition. It gives me immutability by default, value-based equality (great for unit testing comparisons), and a generic ToString() implementation out of the box. It has fundamentally sped up how I prototype and define domain models.

3. Global Usings (C# 10)

How many times have you typed (or let Visual Studio auto-generate) these lines?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

You do this in *every single file. It’s visual noise.

The New Way: You can now create a single file (often called GlobalUsings.cs) in your project root:

// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using Microsoft.EntityFrameworkCore;

Alternatively, you can enable “Implicit Usings” in your .csproj file, which handles the basics for you automatically.

Why it changed my workflow: My files now start directly with the code that matters. I only see using statements if they are specific to that file (like a specific Service or Utility). It drastically reduces the header clutter.

4. Switch Expressions (C# 8)

The traditional switch statement was clunky. It required casebreak, and return keywords scattered everywhere. It was easy to miss a break statement and cause a bug.

The Old Way:

public string GetStatusMessage(OrderStatus status)
{
    switch (status)
    {
        case OrderStatus.Pending:
            return "Please wait.";
        case OrderStatus.Shipped:
            return "On the way!";
        case OrderStatus.Delivered:
            return "Enjoy your item.";
        default:
            throw new ArgumentException("Unknown status");
    }
}

The New Way: Switch expressions utilize pattern matching to turn this into a functional-style expression.

public string GetStatusMessage(OrderStatus status) => status switch
{
    OrderStatus.Pending   => "Please wait.",
    OrderStatus.Shipped   => "On the way!",
    OrderStatus.Delivered => "Enjoy your item.",
    _                     => throw new ArgumentException("Unknown status")
};

Why it changed my workflow: It’s concise and forces me to think in terms of expressions (returning a value) rather than statements (executing a flow). The compiler also helps ensure I’ve covered all enum cases. It turns 12 lines of procedural logic into 5 lines of readable mapping.

5. Raw String Literals (C# 11)

For years, writing JSON, SQL, or HTML inside a C# string was painful because of “escaping.” You had to escape double quotes (\") constantly.

The Old Way:

var json = "{\n" +
           "  \"name\": \"John\",\n" +
           "  \"age\": 30\n" +
           "}";
// Or slightly better with Verbatim strings, but you still double-up quotes
var json2 = $@"{{
  ""name"": ""{name}"",
  ""age"": 30
}}";

The New Way: Raw string literals use three double quotes ("""). You can paste anything inside, and you don’t need to escape quotes.

var json = $$"""
{
  "name": "{{name}}",
  "age": 30
}
""";

Why it changed my workflow: I work with SQL queries and JSON payloads daily. Being able to copy a JSON object from Postman and paste it directly into C# without spending 5 minutes fixing escape characters is a massive quality-of-life improvement.

6. Conclusion

Some developers argue that these are just “syntactic sugar.” I disagree.

When you remove the noise — the braces, the boilerplate imports, the manual constructors, the escaped quotes — you are left with the intent of the code.

Modern C# allows me to write code that reads almost like a requirement document. If you are still sticking to the “old ways” because of muscle memory, I highly recommend trying these features in your next refactor. You won’t want to go back.

Are there other C# features that have become indispensable to you? Let me know in the comments!

Loading

<p>The post Goodbye Verbose Code: 5 Modern C# Features That Changed My Workflow first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2025/11/goodbye-verbose-code-5-modern-c-features-that-changed-my-workflow/feed/ 6
Setup Launch Debug for the ASP.NET Core Project in VS Code https://www.coderblog.in/2025/10/setup-launch-debug-for-the-asp-net-core-project-in-vs-code/ https://www.coderblog.in/2025/10/setup-launch-debug-for-the-asp-net-core-project-in-vs-code/#comments Fri, 03 Oct 2025 01:01:17 +0000 https://www.coderblog.in/?p=1358 1. Introduction VS Code is a great IDE for programmers. You can completely use it to develop .Net

<p>The post Setup Launch Debug for the ASP.NET Core Project in VS Code first appeared on Coder Blog.</p>

]]>
1. Introduction

VS Code is a great IDE for programmers. You can completely use it to develop .Net Core projects. It is lighter and faster than Visual Studio, and the powerful extensions can help you do almost everything you need!

By the way, if you are not Medium member, please click here to read.

You can use C# Dev Kit to easily develop the .Net project, it can provide you with intelligent code hints, and can provide the code auto-complete with AI

Ok, let’s get back to the point. If you want to debug the ASP.NET project in VS Code, you need to create a launch.json file, and the point is how can you setup a different environment in debug mode? and let VS Code auto launch the default browser for the page you want to debug?

2. Auto launch the debug page

By default, the launch.json file should look like below

Yes, if you just want to launch a .Net debug mode, that’s enough. But we want more!

For the above sample, this is a .Net Core API project, it’s using the swagger for API doc, and we want to auto launch the browser and redirect to the swagger index page, so we can add the below

"serverReadyAction": {
"action": "openExternally",
"pattern": "Now listening on:\\s+\"(http?://\\S+)\"",
"uriFormat": "%s/swagger/index.html"
},

serverReadyAction can define the action after launch the debug mode, and use the pattern to match the URL in console, and then redirect to the URL with uriFormat

For example, you will find the following in VS Code console after pressing F5 in debug mode:

3. Define the environment

For the business project, there should be a UAT and a Production version (or more versions), and most of them just have different configurations, so we can use below for setup the current debug environment

"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:7041;http://localhost:5171"
},

The ASPNETCORE_URLS can be used for the launch debug page. After set the ASPNETCORE_ENVIRONMENT , it will use the difference appsettings.json based on this setting, suppose there are 3 appsettings.json files below

appsettings.json
appsettings.Development.json
appsettings.Production.json

The complete launch.json should be like below

4. Conclusion

I think VS Code can completely replace Visual Studio after .Net 6.0 for web development. The debug mode can be easily to fulfill most of situations, and I really love the intelligent code hints.

Loading

<p>The post Setup Launch Debug for the ASP.NET Core Project in VS Code first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2025/10/setup-launch-debug-for-the-asp-net-core-project-in-vs-code/feed/ 18
How to use Quartz.NET for job scheduling on ASP.NET Core https://www.coderblog.in/2025/06/how-to-use-quartz-net-for-job-scheduling-on-asp-net-core/ https://www.coderblog.in/2025/06/how-to-use-quartz-net-for-job-scheduling-on-asp-net-core/#comments Tue, 03 Jun 2025 04:02:25 +0000 https://www.coderblog.in/?p=1262 1. Introduction Quartz.NET is a powerful library that help to create the schedule job on .Net project, you can

<p>The post How to use Quartz.NET for job scheduling on ASP.NET Core first appeared on Coder Blog.</p>

]]>
1. Introduction

Quartz.NET is a powerful library that help to create the schedule job on .Net project, you can setup and execute the schedule job on IIS, that will be very convenient for a web application. But there is a problem for running schedule job on IIS, if there is no access for a long time in a website, IIS will be stop running the schedule job, but don’t worry, I will let you know how to fix this issue, and I will show you how to create a setting to control the schedule job.

2. Install Quartz.NET

You can easy to install Quartz.NET from NuGet Package in Visual Studio, just search Quartz in Manage NuGet Packages and choose Quartz.NET to install.

Or you also can execute below command to install if you are using Visual Studio Code

Install-Package Quartz

3. Usage

Actually, you can find the detail usage from Quartz.NET official website, so today, I will show you how to use Quartz.NET as a background service in ASP.NET Core project, and you can easy setup it in your app.settings

  1. Create Schedule Model

We need to create the model for app.settings mapping

//JobSettings.cs

namespace API.Schedules
{
    /// <summary>
    /// Schedule Job Settings Section in appsettings.json
    /// </summary>
    public class JobSettings
    {
        public List<JobDetail> Jobs { get; set; }
        public JobSettings()
        {
            Jobs = new List<JobDetail>();
        }
    }

    /// <summary>
    /// Schedule Job Detail
    /// </summary>
    public class JobDetail
    {
        /// <summary>
        /// The type name of the job's class for reference
        /// </summary>
        /// <value></value>
        public required string TypeName { get; set; }
        /// <summary>
        /// The unique key of the job
        /// </summary>
        /// <value></value>
        public required string JobKey { get; set; }
        /// <summary>
        /// Set the schedule with cron expressions
        /// </summary>
        /// <value></value>
        public string? CronSchedule { get; set; }
        /// <summary>
        /// Schedule interval in seconds
        /// </summary>
        /// <value></value>
        public int? IntervalSeconds { get; set; }
        /// <summary>
        /// Schedule interval in minutes
        /// </summary>
        /// <value></value>
        public int? IntervalInMinutes { get; set; }
        /// <summary>
        /// Schedule interval in hours
        /// </summary>
        /// <value></value>
        public int? IntervalInHours { get; set; }
        /// <summary>
        /// Whether the job is active
        /// </summary>
        /// <value></value>
        public bool IsActive { get; set; }
    }
}
  1. Create the Schedule Service

Create the schedule service for register background service

//ScheduleService.cs

using Quartz;

namespace API.Schedules
{
    public static class ScheduleService
    {
        public static void RegisterBackgroundServices(this IServiceCollection services, JobSettings jobSettings)
        {
            // Register the job settings
            jobSettings.Jobs.Where(s => s.IsActive).ToList().ForEach(setting =>
            {
                // Use a Scoped container to create jobs. This is necessary to inject services into the job.
                services.AddQuartz(options =>
                {
                    // Register the job with TypeName
                    Type? _f = Type.GetType(setting.TypeName);
                    if (_f != null)
                    {
                        var jobKey = JobKey.Create(setting.JobKey);

                        if (!string.IsNullOrEmpty(setting.CronSchedule))
                            //trigger with cron expressions
                            options.AddJob(_f, jobKey).AddTrigger(trigger => trigger.ForJob(jobKey).WithCronSchedule(setting.CronSchedule).StartNow());
                        else if (setting.IntervalSeconds.HasValue)
                            //trigger with IntervalSeconds
                            options.AddJob(_f, jobKey).AddTrigger(trigger => trigger.ForJob(jobKey)
                                .WithSimpleSchedule(s => s.WithIntervalInSeconds(setting.IntervalSeconds.Value).RepeatForever()));
                        else if (setting.IntervalInMinutes.HasValue)
                            //trigger with minutes
                            options.AddJob(_f, jobKey).AddTrigger(trigger => trigger.ForJob(jobKey)
                                .WithSimpleSchedule(s => s.WithIntervalInMinutes(setting.IntervalInMinutes.Value).RepeatForever()));
                        else if (setting.IntervalInHours.HasValue)
                            //trigger with hours
                            options.AddJob(_f, jobKey).AddTrigger(trigger => trigger.ForJob(jobKey)
                                .WithSimpleSchedule(s => s.WithIntervalInHours(setting.IntervalInHours.Value).RepeatForever()));
                    }
                });
            });
            // Add the Quartz services
            services.AddQuartzHostedService(options =>
            {
                options.WaitForJobsToComplete = true;
                options.AwaitApplicationStarted = true;
            });
        }
    }
}

and register schedule job settings in Program.cs file

builder.Services.Configure<JobSettings>(builder.Configuration.GetSection("JobSetting"));

builder.Services.RegisterBackgroundServices(builder.Configuration.GetSection("JobSetting").Get<JobSettings>() ?? new JobSettings());
  1. Add the settings

We can add the JobSetting section into app.settings for easy to control the schedule jobs

  "JobSetting": {
    "Jobs": [
      {
        "TypeName": "API.Schedules.CronBgJob, API",
        "JobKey": "CronScheduler",
        "CronSchedule": "0 30 9,14,16,23,7 * * ?", //trigger the job every day at 9:30,14:30...
        "IsActive": true
      },
      {
        "TypeName": "API.Schedules.SimpleBgJob, API",
        "JobKey": "SimpleScheduler",
        "IntervalInMinutes": 10,  //trigger the job in every 10 minutes
        "IsActive": true
      }
    ]
  },

There are two jobs of the above settings, one for cron job with cron expressions, you can find the details how to use it, another is a simple job, it will trigger in every 10 minutes when the application is startup.

Of course, you can also set the interval with seconds or hours in the settings, because we already handled these in the schedule job service.

  1. Create the jobs

Now we can create the jobs. First we create a cron job below

//CronBgJob.cs

using Quartz;
namespace API.Schedules
{
    [DisallowConcurrentExecution]
    public class CronBgJob : IJob
    {    
        private readonly ILogger<CronBgJob> _logger;
    
        public CronBgJob(ILogger<CronBgJob> logger)
        {
            _logger = logger;
        }
    
        public Task Execute(IJobExecutionContext context)
        {
            _logger.LogDebug("this is a cron job ");
            Console.WriteLine("testing cron job log in schedule");
            return Task.CompletedTask;
        }
    }
}

Create a simple job

//SimpleBgJob.cs

using Quartz;
namespace API.Schedules
{
    [DisallowConcurrentExecution]
    public class SimpleBgJob : IJob
    {    
        private readonly ILogger<SimpleBgJob> _logger;
    
        public SimpleBgJob(ILogger<SimpleBgJob> logger)
        {
            _logger = logger;
        }
    
        public Task Execute(IJobExecutionContext context)
        {
            _logger.LogDebug("this is a simple job ");
            Console.WriteLine("testing simple job log in schedule");
            return Task.CompletedTask;
        }
    }
}

As you can see, there is no any difference with these jobs except the class name, so the main point is the app.settings to define the job’s type and how to trigger them.

4. Setup the IIS

By default IIS will recycle and stop the app pools from time to time, that’s mean if you start the Quartz with a web application, the scheduler might get disposed later on due to site inactivity. But you are using IIS8, you can setup the IIS to let it keep running to resolve this issue.

1) Install the Application Initialization module in IIS

2) Go to IIS website advanced settings, set the Preload Enabled to True

The `Preload Enabled` setting along with the start mode setting can be used to ‘warm up’ your web application.

3) Go to the application pool advanced settings, set the Start Mode to AlwaysRunning

When you set the startMode property of your application pool to AlwaysRunning a worker process is spawned as soon as IIS starts up and does not wait for the first user request.

5. Conclusion

Quartz.NET is a great library for handle schedule job of .Net project, you can use the cron expressions to set the schedule, and there are still many features you can find in the official guideline, you just need to pay attention to the IIS recycle issue, it will stop your job, but you can also fix it base on the above settings.

In the end, if you enjoyed this article, please follow me here on Medium for more stories about .Net Core, Angular, and other Tech!

Loading

<p>The post How to use Quartz.NET for job scheduling on ASP.NET Core first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2025/06/how-to-use-quartz-net-for-job-scheduling-on-asp-net-core/feed/ 8
How to Deploy DeepSeek Locally https://www.coderblog.in/2025/05/how-to-deploy-deepseek-locally/ https://www.coderblog.in/2025/05/how-to-deploy-deepseek-locally/#comments Thu, 22 May 2025 03:28:02 +0000 https://www.coderblog.in/?p=1279 1. Introduction DeepSeek has become very popular recently. You can use it for free online. However, their server

<p>The post How to Deploy DeepSeek Locally first appeared on Coder Blog.</p>

]]>
1. Introduction

DeepSeek has become very popular recently. You can use it for free online. However, their server is still unstable, and it may stop working just as you’re happily in the middle of something. So, I think you can try downloading the model to use it, but of course, there will be some limitations to that, which I will show you later.

2. Which Models Should You Use Locally?

You can’t choose which version to use in the DeepSeek online version, but you can download other versions to use on your computer locally.

Ok, but there are many types of models of DeepSeek below:

DeepSeek LLM
DeepSeek Coder
DeepSeek Math
DeepSeek VL
DeepSeek V2
DeepSeek Coder V2
DeepSeek V3
DeepSeek R1

Even in each model, there are still many types, something like

1.5b, 7b, 8b, 14b, 32b, 70b, 671b, etc.

So, which one should you use? And what’s the difference?

Ok, let me show you!

1) Model Specialization

Different DeepSeek models are optimized for distinct tasks:

DeepSeek Models

2) Parameter Sizes (1.5B, 7B, 33B, etc.)

Larger models generally have stronger performance but require more resources

The AI model parameter sizes

3) Model Versions (V1, V2, V3)

Iterations improve performance, efficiency, or specialization

  • V1: Baseline architecture.
  • V2/V3: Enhanced training data, optimized architectures (e.g., MoE for efficiency), or extended context windows.
  • Example: DeepSeek Coder V2 might support longer code contexts than V1, while DeepSeek V3 could use sparse activation for lower inference costs.

So, if you don’t need to handle some special tasks (e.g. coding or mathematical operations), I suggest you focus on the DeepSeek VL or R1 model. About the parameter sizes, they need to be based on your computer configuration. You can find the below table for your reference:

RAM Requirements for Common Sizes

You can use the below formula to calculate the RAM:

  1. Full-Precision (FP32):
    Memory (GB)=Parameters×4 bytes
    (e.g., 7B parameters = 7×4=28 GB)
  2. Half-Precision (FP16/BF16):
    Memory (GB)=Parameters×2 bytes
    (e.g., 7B parameters = 7×2=14 GB)
  3. 8-bit Quantization:
    Memory (GB)=Parameters×1 byte
    (e.g., 7B parameters = 7×1=7 GB)

For my example, I am using a Mac Mini M4 with 32G RAM and I can use up to the 32B size. But it also needs to be based on what AI Model Management System you are using, for example, if I use LM Studio by default settings with DeepSeek R1 32B Model, it would hang and auto-restart my device, but if I use Chatbox or Open-WebUI then that will be fine, but of course just a little slowly.

A Comprehensive Guide to AI Model Naming Conventions

1. Introduction

blog.stackademic.com

3. How to download the models to use?

1) You can download the AI models from Hugging Face with Python.

In this way, you need to write the codes and handle the UI yourself, but the advantage is you can use all of the models in Hugging Face.

For example, download the DeepSeek 1.5B model below with transformers from Hugging Face:

# import transformers library
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

# the model name in Hugging face
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"

# Load with explicit MPS configuration
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    device_map="mps",  # Directly specify MPS
    torch_dtype=torch.float16,  # Force FP16 for MPS compatibility
    low_cpu_mem_usage=True  # Essential for 8GB RAM
).eval()  # Set to eval mode immediately

You can also use the gradio to generate a simple UI

import gradio as gr

# Define the function to generate text
def generate_text(prompt, max_length=100, temperature=0.7):
    inputs = tokenizer(prompt, return_tensors="pt").to("mps")
    
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_length=max_length,
            temperature=temperature,
            pad_token_id=tokenizer.eos_token_id
        )
    
    return tokenizer.decode(outputs[0], skip_special_tokens=True)

# Create a Gradio interface
demo = gr.Interface(
    fn=generate_text,
    inputs=[
        gr.Textbox(lines=3, placeholder="Enter your prompt..."),
        gr.Slider(50, 500, value=100, label="Max Length"),
        gr.Slider(0.1, 1.0, value=0.7, label="Temperature")
    ],
    outputs="text",
    title="DeepSeek-R1-Distill-Qwen-1.5B Demo",
    description="A distilled 1.5B parameter model for efficient local AI."
)

# Launch the interface
demo.launch(share=True)  
# Access via http://localhost:7860

Run the above codes, you will download the DeepSeek-R1-Distill-Qwen-1.5B model and create a localhost server as below

HTTP://localhost:7860

You will find the result below

DeepSeek R1 Distill 1.5B Demo

But this is not easy to use and not friendly enough.

2) Use Ollama to download the models

This is the easy way to host a local LLMs.

Download and install the Ollama from https://ollama.com/ . After installed the Ollama, you can run the Ollama command in console ( Terminal on MacOS, Command line on Windows)

For example, we want to download the DeepSeek R1 Distill 1.5B model, go to https://ollama.com/search search the DeepSeek model, and you will find many models

Just click the first one (deepseek-r1), choose the tags to 1.5b and click the right side’s copy button

Run the command

ollama run deepseek-r1:1.5b

It will start downloading the model at first time, after the model downloaded, will shoe below and you can input the prompt to use

You will find that also can not be used as well, so we need a LLM management tool for handle the UI to let’s easy to use the model.

The Powerful Novel Generator by AI

1. Introduction

blog.stackademic.com

Go to https://www.chatboxai.app/ to download the latest version.

Open the Chatbox app, click Settings in the left side, and set the model provider to OLLAMA API, it will auto-detect all of the downloaded models in Ollama

Chatbox AI Model Management
Chatbox Settings

When you create a new chat, you can switch to a different model at any times

Seems great now

Ok, there are still other tools for that, such as:

LM Studio: https://lmstudio.ai/

This is also a very nice application for handling AI models, but don’t know why it will cash my OS (auto-reboot) when I try to run the DeepSeek R1 32B model, and that’s fine in Chatbox app.

Another one is Open-Web UI : https://openwebui.com/

It is an extensible, self-hosted (web) AI interface that adapts to your workflow, all while operating entirely offline.

Please let me know if you find another better application 🙂

Loading

<p>The post How to Deploy DeepSeek Locally first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2025/05/how-to-deploy-deepseek-locally/feed/ 8
The Art of Service Registration: Writing Clean and Scalable IServiceCollection Extensions https://www.coderblog.in/2025/05/the-art-of-service-registration-writing-clean-and-scalable-iservicecollection-extensions/ https://www.coderblog.in/2025/05/the-art-of-service-registration-writing-clean-and-scalable-iservicecollection-extensions/#comments Wed, 21 May 2025 01:53:15 +0000 https://www.coderblog.in/?p=1311 1. Introduction IServiceCollection is an interface defined in the Microsoft.Extensions.DependencyInjection namespace. It serves as a collection of ServiceDescriptor objects, each describing how a

<p>The post The Art of Service Registration: Writing Clean and Scalable IServiceCollection Extensions first appeared on Coder Blog.</p>

]]>
1. Introduction

IServiceCollection is an interface defined in the Microsoft.Extensions.DependencyInjection namespace. It serves as a collection of ServiceDescriptor objects, each describing how a particular service should be resolved by the DI container. And each ServiceDescriptor includes:

  • The service type (Type)
  • The implementation type or instance (Type or object)
  • The service’s lifetime (Singleton, Scoped, or Transient)

So this interface is primarily used during application startup to configure which services are available and how they should be instantiated.

We typically use the IServiceCollection inside the ConfigureServices of the Startup class (in .NET 5 and earlier) or in the Program.cs file starting from .NET 6.

In .NET 5 and earlier:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddDbContext<MyDbContext>();
    services.AddTransient<IMyService, MyService>();
}

After .NET 6

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<MyDbContext>();
builder.Services.AddTransient<IMyService, MyService>();

2. Why is IServiceCollection Important?

In both cases, services is an instance of IServiceCollection.

IServiceCollection plays such a central role in ASP.NET Core, because all services are registered through a single point of entry. This makes it easy to manage dependencies and understand what services are available in your application.

By defining extension methods on IServiceCollection, you can encapsulate feature-specific registrations into reusable modules. For example:

services.AddEmailModule();
services.AddPaymentGateway();

Each of these methods might internally register multiple services related to that module.

Most third-party libraries (like Entity Framework Core, Swagger, AutoMapper, etc.) provide extension methods on IServiceCollection. This allows developers to easily integrate complex systems with minimal effort.

Example:

services.AddEntityFrameworkSqlServer();
services.AddSwaggerGen();

We can also conditionally register services based on the environment settings

if (env.IsDevelopment())
{
    services.AddTransient<IDebugService, DebugService>();
}

This flexibility supports different behaviors across environments (development, staging, production).

IServiceCollection integrates seamlessly with the IOptions<T> pattern, which allows you to bind configuration values to strongly-typed objects and inject them into services:

services.Configure<PaymentOptions>(Configuration.GetSection("Payment"));
services.AddSingleton<IPaymentProcessor, PaymentProcessor>();

While ASP.NET Core ships with a built-in lightweight DI container, you can replace it with more advanced containers like Autofac or DryIoc. These custom containers often accept an IServiceCollection to import service registrations before switching over to their own internal mechanisms.

3. Example for using IServiceCollection

1) Create a custom authentication service

public static class AuthenticationExtensions
{
    public static IServiceCollection AddCustomAuthentication(this IServiceCollection services)
    {
        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "JwtBearer";
        })
        .AddJwtBearer("JwtBearer", options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secret-key")),
                ValidateIssuer = false,
                ValidateAudience = false
            };
        });
        return services;
    }
}

and use as below

services.AddCustomAuthentication();

2) Encapsulate the service registration of the infrastructure layer

public static class InfrastructureRegistration
{
    public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(config.GetConnectionString("DefaultConnection")));
        services.AddScoped<IEmailSender, EmailSender>();
        services.AddScoped<ISmsService, SmsService>();
        return services;
    }
}

and use as below

services.AddInfrastructure(Configuration);

3) Encapsulate the integration of third — party libraries

public static class MediatorExtensions
{
    public static IServiceCollection AddMediatorHandlers(this IServiceCollection services)
    {
        //ingegrate the MediatR
        services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>));
        return services;
    }
}

and use as below

services.AddMediatorHandlers();

4) Create the service with options

public static IServiceCollection AddMyServiceWithConfig(this IServiceCollection services, Action<MyOptions> configure)
{
    var options = new MyOptions();
    configure(options);
    services.AddSingleton(options);
    services.AddTransient<IMyService, MyService>();
    return services;
}

Use as below

services.AddMyServiceWithConfig(opt =>
{
    opt.Timeout = TimeSpan.FromSeconds(30);
    opt.MaxRetries = 3;
});

5) Create the service with the environmental conditions

public static IServiceCollection AddDevelopmentOnlyServices(this IServiceCollection services, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        services.AddTransient<IDebugService, DebugService>();
    }
    return services;
}

Use as below

services.AddDevelopmentOnlyServices(env);

4. Questions

Q: What are some common mistakes people make when using IServiceCollection?

Answer:

  1. Putting all registration logic directly in ConfigureServices or Program.cs
  • ❌ Leads to bloated, unreadable startup code
  • ✅ Encapsulate registrations into modular extension methods

2. Not understanding service lifetimes

  • ❌ Registering a Scoped service into a Singleton can cause memory leaks or incorrect state sharing
  • ✅ Understand the difference between TransientScoped, and Singleton Lifetimes and use them appropriately

3. Overusing AddSingleton<T>()

  • ❌ Misuse of Singleton can lead to shared mutable state issues
  • ✅ Only use Singleton for truly global, stateless services

4. Manually registering too many concrete types

  • ❌ Writing many services.AddTransient<...> lines become hard to maintain
  • ✅ Use convention-based or reflection-based auto-registration instead

5. Ignoring registration order or conditional logic

  • ❌ Later middleware or components may depend on unregistered services
  • ✅ Be mindful of registration order, especially when integrating third-party libraries

6. Misusing IServiceProvider to manually resolve services (Service Locator Anti-pattern)

  • ❌ Manually calling provider.GetService() is an anti-pattern
  • ✅ Always rely on constructor injection instead

Q: How can you ensure your service registrations are as clean and scalable as possible?

Answer:

  1. We can encapsulate registration logic with extension methods
public static class ApplicationModuleExtensions
{
    public static IServiceCollection AddApplicationCore(this IServiceCollection services)
    {
        services.AddScoped<IMediator, Mediator>();
        services.AddTransient<IValidator<Order>, OrderValidator>();
        return services;
    }
}

Then call it below

services.AddApplicationCore()
        .AddInfrastructure(Configuration)
        .AddEmailServices();

This can keep the startup code clean and readable.

2. Organize registration by feature or layer

Group related services into modules:

services.AddDataAccess()

services.AddApplicationServices()

services.AddExternalIntegrations()

services.AddCustomMiddlewares()

This improves clarity and supports team collaboration.

3. Use configuration options (IOptions) to parameterize services

services.Configure<PaymentSettings>(Configuration.GetSection("Payment"));
services.AddSingleton<IPaymentProcessor, PaymentProcessor>();

This makes your services more configurable and testable.

4. Avoid hardcoded dependencies

  • ❌ Directly instantiating objects or using concrete types in registrations
  • ✅ Favor interfaces over implementations to improve flexibility and testability

5. Automate registration with convention-based approaches

For example, use reflection to register multiple services at once:

var assembly = typeof(MyService).Assembly;

services.Scan(scan => scan
    .FromAssemblies(assembly)
    .AddClasses(classes => classes.InNamespaces("MyApp.Application.Services"))
    .AsImplementedInterfaces()
    .WithScopedLifetime());

6. Design for modularity with Feature Folders or Plugin architecture

  • Each module handles its own service registration
  • The main application simply composes these modules together

4. Conclusion

IServiceCollection is the central mechanism for registering services in ASP.NET Core applications, enabling dependency injection throughout the application.

It allows developers to cleanly encapsulate service registrations using extension methods, promoting modular and reusable code.

When using IServiceCollection, It’s important to understand service lifetimes (Transient, Scoped, Singleton) and to avoid anti-patterns such as registering concrete types unnecessarily or mismanaging dependencies.

It also supports conditional registration based on environment or configuration, making it flexible for different deployment scenarios.

Proper use of IServiceCollection leads to maintainable, testable, and scalable applications.

Loading

<p>The post The Art of Service Registration: Writing Clean and Scalable IServiceCollection Extensions first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2025/05/the-art-of-service-registration-writing-clean-and-scalable-iservicecollection-extensions/feed/ 3
What a magic to let you easily modify any website pages https://www.coderblog.in/2025/05/what-a-magic-to-let-you-easily-modify-any-website-pages/ https://www.coderblog.in/2025/05/what-a-magic-to-let-you-easily-modify-any-website-pages/#comments Sat, 03 May 2025 01:06:55 +0000 https://www.coderblog.in/?p=1300 Introduction Do you think that you can modify any website page’s content? For example, you can easily modify

<p>The post What a magic to let you easily modify any website pages first appeared on Coder Blog.</p>

]]>
Introduction

Do you think that you can modify any website page’s content? For example, you can easily modify Microsoft’s web page. 😁

Please wait, don’t be excited, I’m not going to teach you how to hack a website, I just want to introduce a technique that you can use to do that!

You can see the demo below:

Actually that’s easy to do, just one line command that you can modify the webpage what you want.😆

How to do

It’s hard to imagine that such a cool feature only requires one command in the console:

Ok, let’s do it!

Input the below JS command in Chrome console

document.designMode="on";

and then you will find the magic:

But I also need to let you know that this approach can only modify the webpage content in your local😅

What’s the designMode

Ok, now you know how to modify the webpage on runtime, but what’s this command?

from the MDN Docs description:

document.designMode controls whether the entire document is editable. Valid values are "on" and "off". According to the specification, this property is meant to default to "off". Firefox follows this standard. The earlier versions of Chrome and IE default to "inherit". Starting in Chrome 43, the default is "off" and "inherit" is no longer supported. In IE6-10, the value is capitalized.

and you can find the browser compatibility below

With this API, we can also edit Iframe nested pages:

iframeNode.contentDocument.designMode = "on";

Association API

Other APIs associated with designMode include contentEditable and execCommand (deprecated, but still available in some browsers).
contentEditable is similar to designMode, but contentEditable can make specific DOM elements editable, while designMode can only make the entire document editable.

The document.execCommand() method allows us to format, edit, or manipulate content in a web page. It is mainly used to operate editable content on a web page (such as <textarea> or elements set by setting the contentEditable or designMode attributes to “true”), such as bolding text, inserting links, adjusting font styles, etc. Since it has been deprecated by W3C, it is no longer introduced in this article.

Conclusion

This command is not only for fun, you can quickly to modify the webpage to show a demo to your client, or help the designer to know what needs to be changed in the page. Also, if you want to refer to the design of other websites, then make some modifications based on them to suit your needs. This command can also be helpful!

Loading

<p>The post What a magic to let you easily modify any website pages first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2025/05/what-a-magic-to-let-you-easily-modify-any-website-pages/feed/ 13
The Powerful Novel Generator by AI https://www.coderblog.in/2025/03/the-powerful-novel-generator-by-ai/ https://www.coderblog.in/2025/03/the-powerful-novel-generator-by-ai/#comments Tue, 04 Mar 2025 03:08:13 +0000 https://www.coderblog.in/?p=1284 1. Introduction Do you want to write a novel to sell on Amazon with KDP but don’t know

<p>The post The Powerful Novel Generator by AI first appeared on Coder Blog.</p>

]]>
1. Introduction

Do you want to write a novel to sell on Amazon with KDP but don’t know how to start?

I think maybe you will try to ask AI for help with that! But as you know, there are many limitations for AI to generate novel, actually, this is not easy to do if you want to generate a good novel structure and content from AI.

But don’t worry, I will show you an application that can help you easily generate a professional novel.

2. Novel Generator

You can find the below core functions of the application:

The main screen is as below

3. How to use:

1) Setup the LLM Model

First, you need to set up the LLM Model information, it supports many of the LLM services:

So you can also use the Ollama or MS Studio for the localhost LLM models.

For the embedding model, I will suggest using the bge-m3 and host by Ollama, because it can support large input for your local version

after that you can click “Test configuration” to make sure the LLM model is working.

2) Input the novel information

You can set many things here, but the main items are as below what I input

First, you must input the topic of your novel, and input the type of the novel, then set how many chapters you want to generate, and the number of words, at the last, don’t forget to set the path to save of the output files.

3) Generate architecture

The application is based on “Snowflake Writing Method” to generate the novel

So it will generate the novel architecture first as below steps :

a. Generate a core seed base on your topic (you can input it)
b. Design 3–6 core characters with dynamic change potential, core driving force triangle, and the relationship conflict network
c. Design the word-view with the Three-dimensional Interweaving Method

The Three-Dimensional Interweaving Method is a narrative technique used in writing to create depth and complexity by interweaving elements across three key dimensions: Time, Space, and Theme. This method allows writers to build layered stories that engage readers on multiple levels, enhancing both the emotional and intellectual impact of the narrative.

d. Generate the plot structure with three-act suspense

Click Step1. Generate architecture will generate the below result

and will also generate the Character State

4) Generate Outline

After the architecture is generated, you can generate the novel outline with the below format by clicking Step2. Generate outline

5) Generate Draft

Ok, you can generate a draft content based on the outline one by one. Click Step3. Generate dratf button, it will pop up to let you confirm whether everything is ok, but don’t forget to set the Chapter number value to the chapter you want to generate:

after done, the content will be shown in the main window

6) Expand and finalize the manuscript

The final step is to finalize the manuscript. Click Step4. Final chapter will help to do that.

There is a great function that will detect the current number of words whether fulfills your setting, if no, then will pop up to let you choose whether you need to expand the content

After finalizing the manuscript, it will generate the global summary for this chapter

You can also find all the chapters in Chapters Manage

4. Get the Novel Generator

I think you can’t wait any longer, ok, this is a free and open-source application built by Python, and you can get it in the link below:

This is another version enhanced by me, but you can find the original version below:

The original version does not support English, so that’s why I created another version and added the below new features:

a. Supports the generation of novels in multiple languages, and the interface also supports multiple languages. Currently, only Chinese and English are available, but other languages can be added easily.

b. Automatically load the current chapter every time it starts

c. The expansion function has been optimized. The original version can only be expanded once and the original content is rewritten each time. However, due to the limitations of the AI model, it is impossible to write too long content once. Therefore, my approach now is to continue writing on the original content and superimpose the new content every time. In this way, after many operations, a long enough content can be generated.

d. English words counting is also supported during expansion

e. Add a button to each content page to facilitate the copying of the current content directly

5. Conclusion

This Novel Generator is very powerful, but I suggest you also need to do some modification for the generated contents, AI just helps you to find more ideas and let you easily do your work, but not 100% replace your works 🙂

Loading

<p>The post The Powerful Novel Generator by AI first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2025/03/the-powerful-novel-generator-by-ai/feed/ 3
How to Use Runtime Messages in Asp.net Core https://www.coderblog.in/2024/11/how-to-use-runtime-messages-in-asp-net-core/ https://www.coderblog.in/2024/11/how-to-use-runtime-messages-in-asp-net-core/#comments Tue, 19 Nov 2024 15:27:49 +0000 https://www.coderblog.in/?p=1251 1. Introduction When you are creating the APIs, you should need to handle many messages and return them

<p>The post How to Use Runtime Messages in Asp.net Core first appeared on Coder Blog.</p>

]]>
1. Introduction

When you are creating the APIs, you should need to handle many messages and return them to the client, for example, if there is an error or can’t find the data, you need to return the corresponding messages, you can hardcode these messages in the API functions, but just thinking about if your API in the UAT and maybe users want to change the messages again and again, and you will need to find these messages in codes and re-publish them again and again.

If we put all of the messages in the XML files and can be updated in runtime, then we don’t need to change and re-publish the source code, that’s would be great, right?

Ok, let’s start!

2. Define the XML file

As we want to put the messages into XML file, we need to define the XML format, we can base on the XML attribute to get the node’s value, so the format as below

<?xml version="1.0" encoding="utf-8" ?>
<Messages>
 <Item id="NoDataFound">There is no data found!</Item>
 <Item id="InvalidDateFormat">The date value is invalid!</Item>
</Messages>

We can split the XML file based on each function, this can be easier to manage and you can find the message quickly, for example:

Common.xml
User.xml

3. May the key of messages

There is an id attribute in our XML file, we need to get the message based on this id, but I don’t want to hardcode it so we need to create mapping keys below

public static class MsgKey
{
    public static class Common
    {
        public const string NoDataFound = "NoDataFound";
        public const string InvalidDateFormat = "InvalidDateFormat";
    }

    public static class User
    {
        public const string NoUserFound = "NoUserFound";
        public const string UserExist = "UserExist";
        public const string UserInvalid = "UserInvalid";
    }
}

4. Create the message helper

We need to handle the message from different XML files, so we need to load all files in runtime

//load specify XML files based on the function or section
List<string> xmlFiles = files.Split(',').ToList();
foreach (var file in xmlFiles)
{
    //handle the XML file one by one
    LoadMessages(file);
}

Handle the XML file

private void LoadMessages(string xmlFile)
{
    var filePath = Path.Combine(Directory.GetCurrentDirectory(), $"Messages\\{xmlFile}.xml");
    var xdoc = XDocument.Load(filePath);
    var items = xdoc.Descendants("Item")
              .Select(item => new
              {
                  Id = item.Attribute("id").Value,
                  Value = item.Value
              });
    foreach (var msg in items)
    {
        var id = msg.Id;
        var message = msg.Value;
        if (id != null && !Msg.ContainsKey(id))
        {
            Msg[id] = message;
        }
    }
}

the completed codes below

using System.Xml.Linq;

public class MessageHelper
{
    public MessageHelper() { }

    public Dictionary<string, string> Msg { get; private set; }

    public void Init(string files)
    {
        Msg = new Dictionary<string, string>();

        List<string> xmlFiles = files.Split(',').ToList();
        foreach (var file in xmlFiles)
        {
            LoadMessages(file);
        }
    }

    private void LoadMessages(string xmlFile)
    {
        var filePath = Path.Combine(Directory.GetCurrentDirectory(), $"Messages\\{xmlFile}.xml");
        var xdoc = XDocument.Load(filePath);

        var items = xdoc.Descendants("Item")
                  .Select(item => new
                  {
                      Id = item.Attribute("id").Value,
                      Value = item.Value
                  });

        foreach (var msg in items)
        {
            var id = msg.Id;
            var message = msg.Value;
            if (id != null && !Msg.ContainsKey(id))
            {
                Msg[id] = message;
            }
        }
    }
}

5. Usage

1) Register the message helper in program.cs

builder.Services.AddScoped<MessageHelper>();

2) Create a testing API controller MessageTesterController, inject the message helper and pass the XML file name

protected readonly ILogger<MessageTesterController> _logger;
private MessageHelper _messageHelper;
public MessageTesterController(ILogger<MessageTesterController> logger, MessageHelper messageHelper)
{
    this._logger = logger;
     _messageHelper = messageHelper;
     //pass the XML file name in Message folder
     _messageHelper.Init("Common,User");
}

Use the key to get the message:

var noDataFoundMsg = _messageHelper.Msg[MsgKey.Common.NoDataFound];
var userInvalidMsg = _messageHelper.Msg[MsgKey.User.UserInvalid];

The completed codes below:

namespace MyDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class MessageTesterController : ControllerBase
    {
        protected readonly ILogger<MessageTesterController> _logger;

        private MessageHelper _messageHelper;

        public MessageTesterController(ILogger<MessageTesterController> logger, MessageHelper messageHelper)
        {
            this._logger = logger;
            _messageHelper = messageHelper;
            _messageHelper.Init("Common,User");
        }

        /// <summary>
        /// Get the dynamic messages
        /// </summary>
        /// <returns></returns>
        [HttpGet("get-message")]
        public async Task<ActionResult<Object>> GetMessage()
        {
            var apiResult = new ApiResult<Object>();

            try
            {
                var noDataFoundMsg = _messageHelper.Msg[MsgKey.Common.NoDataFound];
                var userInvalidMsg = _messageHelper.Msg[MsgKey.User.UserInvalid];

                _logger.LogDebug("noDataFoundMsg ================ {0}", noDataFoundMsg);
                _logger.LogDebug("userInvalidMsg ================ {0}", userInvalidMsg);

                var returnObj = new
                {
                    noDataFound = noDataFoundMsg,
                    userInvalid = userInvalidMsg
                };

                apiResult.Data = returnObj;

                return Ok(apiResult);

            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "MessageTester Exception");
                apiResult.Success = false;
                apiResult.Message = ex.Message;
                return StatusCode(500, apiResult);
            }
        }

    }
}

And you can find the result in the log file:

6. Conclusion

Put the messages in XML files can be easy to manage, and you can update the messages in runtime, this can save you time to re-build and publish your project again and again, also, you can handle the internationalization with this approach, please feel free to let me know if you have any other ideas 🙂

And you can find the completed source code and demo project on my github.

Loading

<p>The post How to Use Runtime Messages in Asp.net Core first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2024/11/how-to-use-runtime-messages-in-asp-net-core/feed/ 18