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
ASP.NET – 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
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
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
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
How to support multiple appsettings in asp.net 8 https://www.coderblog.in/2024/07/how-to-support-multiple-appsettings-in-asp-net-8/ https://www.coderblog.in/2024/07/how-to-support-multiple-appsettings-in-asp-net-8/#comments Mon, 29 Jul 2024 13:36:04 +0000 https://www.coderblog.in/?p=1232 1. Introduction Most of time, we need to publish a .Net Core project to difference environment with corresponding

<p>The post How to support multiple appsettings in asp.net 8 first appeared on Coder Blog.</p>

]]>
1. Introduction

Most of time, we need to publish a .Net Core project to difference environment with corresponding configuration, so we should something config files like below

appsettings.dev.json
appsettings.uat.json
appsettings.demo.json
appsettings.prod.json

by default, .Net program only get the configuration value from appsettings.json, so how can we read the specified configuration from these files? and how to make sure only one environment configuration file to be copied to the publish folder?

2. What I want to do

There are many tutorials shows you how to use ASPNETCORE_ENVIRONMENT to handle multiple environments, for this approach you need to define or update ASPNETCORE_ENVIRONMENT when you launch or publish the project, but I just want to publish the project with a command and don’t need to change anything (including any config files).

Because I want to deploy the project to difference server and don’t want to change their configuration or environment anymore, this can make sure the deployment source code is stable and avoid to update to wrong environment variables.

So I suppose the publish flow should be just executed the publish command and then copy the release folder to server.

3. Setup multiple environments configuration file

For do that, we need to load the specified environment config file with below in Program.cs

builder.Configuration.AddJsonFile('appsettings.dev.json', optional: true, reloadOnChange: true);

and use the precompiled command to load difference items

#if DEV
builder.Configuration.AddJsonFile('appsettings.dev.json', optional: true, reloadOnChange: true);
#elif DEMO
builder.Configuration.AddJsonFile('appsettings.demo.json', optional: true, reloadOnChange: true);
#elif UAT
builder.Configuration.AddJsonFile('appsettings.uat.json', optional: true, reloadOnChange: true);
#elif PROD
builder.Configuration.AddJsonFile('appsettings.prod.json', optional: true, reloadOnChange: true);
#endif

but wait, for using these precompiled constants, we need to define the constants in the project *.csproj file

  <PropertyGroup Condition=" '$(Configuration)' == 'demo'">
    <DefineConstants>DEMO</DefineConstants>
  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)' == 'dev'">
    <DefineConstants>DEV</DefineConstants>
  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)' == 'uat'">
    <DefineConstants>UAT</DefineConstants>
  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)' == 'prod'">
    <DefineConstants>PROD</DefineConstants>
  </PropertyGroup>

We can define custom constant base on the condition, and the $(Configuration) value is passed from the build command (I will show you later).

In this case, we will find several appsettings files(all environments file) in publish folder, but we should only need one file for current environment, so we need to continue to add below code in *.csproj file

  <ItemGroup Condition=" '$(Configuration)' == 'dev'">
    <Content Remove="appsettings.demo.json" />
    <Content Remove="appsettings.uat.json" />
    <Content Remove="appsettings.prod.json" />
  </ItemGroup>
  <ItemGroup Condition=" '$(Configuration)' == 'demo'">
    <Content Remove="appsettings.dev.json" />
    <Content Remove="appsettings.uat.json" />
    <Content Remove="appsettings.prod.json" />
  </ItemGroup>
  <ItemGroup Condition=" '$(Configuration)' == 'uat'">
    <Content Remove="appsettings.demo.json" />
    <Content Remove="appsettings.dev.json" />
    <Content Remove="appsettings.prod.json" />
  </ItemGroup>
  <ItemGroup Condition=" '$(Configuration)' == 'prod'">
    <Content Remove="appsettings.demo.json" />
    <Content Remove="appsettings.dev.json" />
    <Content Remove="appsettings.uat.json" />
  </ItemGroup>

As you see, we will remove other useless appsettings file base on the condition, so it will only copy the current environment config file to publish folder.

Ok, finally, we can use below command to publish the project base on difference environment

dotnet publish -c demo -r win-x64 -p:PublishReadyToRun=true 

The main point here is -c parameter, this is the $(Configuration) what we use in csproj file.

For example, the above command will only generate below config files in publish folder

appsettings.json
appsettings.demo.json

4. Conclusion

We should use multiple environments configuration for our project, we can simple to use a flag in appsettings.json for that, for example evn:uat, but in this case, we need to change the appsettings.json every time when publish the project, and also need to handle more logic in the source code, to avoid to update the wrong settings in other environments, I suggest to use a publish command for handle difference environment, after executed the command, then will generate the source code to corresponding publish folder, and then we just need to copy these file to server.

Loading

<p>The post How to support multiple appsettings in asp.net 8 first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2024/07/how-to-support-multiple-appsettings-in-asp-net-8/feed/ 11
Generate PDF with Free Spire.PDF in .Net Core and download in Angular https://www.coderblog.in/2023/10/generate-pdf-with-free-spire-pdf-in-net-core-and-download-in-angular/ https://www.coderblog.in/2023/10/generate-pdf-with-free-spire-pdf-in-net-core-and-download-in-angular/#comments Fri, 27 Oct 2023 12:45:24 +0000 https://www.coderblog.in/?p=1107 1. Introduction If you want to handle the PDF in .Net project, I think you would know the

<p>The post Generate PDF with Free Spire.PDF in .Net Core and download in Angular first appeared on Coder Blog.</p>

]]>
1. Introduction

If you want to handle the PDF in .Net project, I think you would know the iTextSharp, it really a powerful PDF handler library, however, it not allow to free to use in commercial after version 2.1.7, and Free Spire.PDF is a Community Edition of the Spire.PDF for .NET, which is a totally free PDF API for commercial and personal use. As a standalone .NET library, Free Spire.PDF for .NET enables developers to create, write, edit, convert, print, handle and read PDF files on any .NET( C#, VB.NET, ASP.NET, .NET Core) applications.

I will base on the previous MyDemo project to demonstrate how to do it.

2. Create the API

2.1 Install Free Spire.PDF from Nuget

First, we need to install the Free Spire.PDF into API project from Nuget

2.2 Create the Controller

Create the GeneratePDFController and action Generate and then create the PDF demo page below

1) Create the pdfDocument and page

//Create a PdfDocument object
PdfDocument doc = new PdfDocument();

//Add a page
PdfPageBase page = doc.Pages.Add();

the elements will be put in the page base on x and y coordinates, so we can define the started coordinates

//Initialize x and y coordinates
float baseX = 100;
float baseY = 30;

create two brush for different font color and create a font

PdfSolidBrush brush1 = new PdfSolidBrush(new PdfRGBColor(Color.Blue));
PdfSolidBrush brush2 = new PdfSolidBrush(new PdfRGBColor(Color.Black));

PdfFont font = new PdfFont(PdfFontFamily.TimesRoman, 12f, PdfFontStyle.Regular);

We want to put a label on left, it can use page.Canvas.DrawString to draw a label, it’s base on the font and brush, and use the PointF to control the position

page.Canvas.DrawString("TextBox:", font, brush1, new PointF(10, baseY));

and draw the TextBox element neat the label on the right, define the position with RectangleF, create the textbox with PdfTextBoxField

RectangleF tbxBounds = new RectangleF(baseX, baseY, 150, 15);
PdfTextBoxField textBox = new PdfTextBoxField(page, "textbox");
textBox.Bounds = tbxBounds;
textBox.Text = "Hello World!";
textBox.Font = font;

in the end, don’t forget add the text box element into PdfDocument, because this is a form element, so use below, and also need to update the y coordinate

doc.Form.Fields.Add(textBox);
baseY += 25;

and then, create two checkboxes in second row

//label
page.Canvas.DrawString("CheckBox:", font, brush1, new PointF(10, baseY));

RectangleF checkboxBound1 = new RectangleF(baseX, baseY, 15, 15);
PdfCheckBoxField checkBoxField1 = new PdfCheckBoxField(page, "checkbox1");
checkBoxField1.Bounds = checkboxBound1;
checkBoxField1.Checked = false;
//checkbox value description
page.Canvas.DrawString("Option 1", font, brush2, new PointF(baseX + 20, baseY));

RectangleF checkboxBound2 = new RectangleF(baseX + 70, baseY, 15, 15);
PdfCheckBoxField checkBoxField2 = new PdfCheckBoxField(page, "checkbox2");
checkBoxField2.Bounds = checkboxBound2;
checkBoxField2.Checked = false;
page.Canvas.DrawString("Option 2", font, brush2, new PointF(baseX + 90, baseY));

//add to the doc form
doc.Form.Fields.Add(checkBoxField1);
doc.Form.Fields.Add(checkBoxField2);

//update y coordinate
baseY += 25;

create the radio buttons

page.Canvas.DrawString("RadioButton:", font, brush1, new PointF(10, baseY));
PdfRadioButtonListField radioButtonListField = new PdfRadioButtonListField(page, "radio");
PdfRadioButtonListItem radioItem1 = new PdfRadioButtonListItem("option1");
RectangleF radioBound1 = new RectangleF(baseX, baseY, 15, 15);
radioItem1.Bounds = radioBound1;

page.Canvas.DrawString("Option 1", font, brush2, new PointF(baseX + 20, baseY));
PdfRadioButtonListItem radioItem2 = new PdfRadioButtonListItem("option2");
RectangleF radioBound2 = new RectangleF(baseX + 70, baseY, 15, 15);
radioItem2.Bounds = radioBound2;

page.Canvas.DrawString("Option 2", font, brush2, new PointF(baseX + 90, baseY));
radioButtonListField.Items.Add(radioItem1);
radioButtonListField.Items.Add(radioItem2);
radioButtonListField.SelectedIndex = 0;

doc.Form.Fields.Add(radioButtonListField);
baseY += 25;

create a combobox

page.Canvas.DrawString("ComboBox:", font, brush1, new PointF(10, baseY));
RectangleF cmbBounds = new RectangleF(baseX, baseY, 150, 15);
PdfComboBoxField comboBoxField = new PdfComboBoxField(page, "combobox");
comboBoxField.Bounds = cmbBounds;
comboBoxField.Items.Add(new PdfListFieldItem("Item 1", "item1"));
comboBoxField.Items.Add(new PdfListFieldItem("Item 2", "itme2"));
comboBoxField.Items.Add(new PdfListFieldItem("Item 3", "item3"));
comboBoxField.Items.Add(new PdfListFieldItem("Item 4", "item4"));
comboBoxField.SelectedIndex = 0;
comboBoxField.Font = font;

doc.Form.Fields.Add(comboBoxField);
baseY += 25;

create a signature field

page.Canvas.DrawString("Signature Field:", font, brush1, new PointF(10, baseY));
PdfSignatureField sgnField = new PdfSignatureField(page, "sgnField");
RectangleF sgnBounds = new RectangleF(baseX, baseY, 150, 80);
sgnField.Bounds = sgnBounds;
doc.Form.Fields.Add(sgnField);
baseY += 90;

you also can create a submit button

page.Canvas.DrawString("Button:", font, brush1, new PointF(10, baseY));
RectangleF btnBounds = new RectangleF(baseX, baseY, 50, 15);
PdfButtonField buttonField = new PdfButtonField(page, "button");
buttonField.Bounds = btnBounds;
buttonField.Text = "Submit";
buttonField.Font = font;

//submit to another website
PdfSubmitAction submitAction = new PdfSubmitAction("https://www.e-iceblue.com/getformvalues.php");
submitAction.DataFormat = SubmitDataFormat.Html;
buttonField.Actions.MouseDown = submitAction;
doc.Form.Fields.Add(buttonField);

save the PDF file and return to client side

var pdfFile = "PDF/FillableForms.pdf";
//Save to file
doc.SaveToFile(pdfFile, FileFormat.PDF);

byte[] pdfBytes = System.IO.File.ReadAllBytes(pdfFile);
MemoryStream stream = new MemoryStream(pdfBytes);

//Delete the file after download 
System.IO.File.Delete(pdfFile);
return new FileStreamResult(stream, "application/pdf");

2.3 Create the client for getting the PDF

We will use the MyDemo project, so just create the generate PDF component

ng g c generatepdf

Create a service for handling API method, only one method for call the API in this case

//MyDemo.Client\src\app\services\generate-pdf.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { config } from 'src/assets/config';

/**
 * ^The generate PDF service for call API to generate PDF
 */
@Injectable({
  providedIn: 'root',
})
export class GeneratePDFService {
  constructor(protected http: HttpClient) {
  }

  /**
   * Generate the TP Grade Appendix PDF
   * @returns
   */
  public generatePDF() {
    var url = config.apiUrl + "/generatePDF/generate";
    return this.http.get(url, { responseType: 'blob' });
  }
}

this is a simple page only a button for generate PDF:

<!-- MyDemo.Client\src\app\generatepdf\generatepdf.component.html -->
<ngx-loading
  #ngxLoading
  [show]="loading"
  [config]="config"
  [template]="loadingTemplate"
></ngx-loading>
<ng-template #loadingTemplate>
  <div class="loading-class">
    <h4>Please wait ...</h4>
  </div>
</ng-template>

<swal #swalBox [swalVisible]="isSwalVisible"></swal>

<h1>Generate PDF</h1>

<div class="row">
  <div class="col-sm-12">
    <button
      class="m-r-8 bg-green-700 text-light"
      mat-raised-button
      (click)="generate()"
    >
      Generate PDF
    </button>
  </div>
</div>
<p></p>

I uses the loading service and swal dialog in this page, you can find how to use them in here and here.

and implement the generate even in ts file

public generate() {
    this.loadingService.start();
    this.generatePDFService.generatePDF().subscribe(blob => {

      if (blob) {
        var downloadURL = window.URL.createObjectURL(new Blob([blob], { type: 'blob' }));
        var link = document.createElement('a');
        link.href = downloadURL;
        link.download = "Demo.pdf";
        link.click();
        this.swalOptions.icon = 'success';
        this.swalOptions.title = 'Generate PDF';
        this.swalOptions.html = `The PDF has been downloaded!`;
        this.swalOptions.showConfirmButton = true;
        this.swalOptions.showCancelButton = false;
        this.swalService.show(this.swalOptions);
      }
      else {
        this.swalOptions.icon = 'error';
        this.swalOptions.title = 'Generate Appendix';
        this.swalOptions.html = `Failed to generate PDF, please find the error in server side!`;
        this.swalOptions.showConfirmButton = true;
        this.swalOptions.showCancelButton = false;
        this.swalService.show(this.swalOptions);
      }

      this.loadingService.stop();

    }, error => {
      console.log('%c [ error ]-52', 'font-size:13px; background:pink; color:#bf2c9f;', error);
      this.loadingService.stop();
    });
  }
}

Done!

You can find the full source code below

https://github.com/coderblog-winson/MyDemo

Loading

<p>The post Generate PDF with Free Spire.PDF in .Net Core and download in Angular first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2023/10/generate-pdf-with-free-spire-pdf-in-net-core-and-download-in-angular/feed/ 11
How to support the JWT in Swagger UI https://www.coderblog.in/2023/09/how-to-support-the-jwt-in-swagger-ui/ https://www.coderblog.in/2023/09/how-to-support-the-jwt-in-swagger-ui/#comments Fri, 15 Sep 2023 07:21:19 +0000 https://www.coderblog.in/?p=1097 1. Introduction Swagger is an open specification for describing and documenting RESTful APIs, it is a standard specification

<p>The post How to support the JWT in Swagger UI first appeared on Coder Blog.</p>

]]>
1. Introduction

Swagger is an open specification for describing and documenting RESTful APIs, it is a standard specification and set of tools for defining, producing, consuming, and visualizing RESTful web services. It makes it easy to design, build, document, and consume APIs efficiently.

But, if we are using the JWT in the API (find more in my previous article), you will find that can’t get data from the API in the Swagger UI, because we also need to pass the JWT to the API to get data, but it seems there is no way to do that, right?

Okay, in this article, I will show you how to do that!

2. Setup the Swagger

We can setup the Swagger in the Program.cs with services:

builder.Services.AddSwaggerGen(options =>
{
    //...
}

So, if we want to support JWT in the UI, just need to add the below codes

builder.Services.AddSwaggerGen(options =>
{
     // add JWT Authentication
    var securityScheme = new OpenApiSecurityScheme
    {
        Name = "JWT Authentication",
        Description = "Enter JWT Bearer token **_only_**",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.Http,
        Scheme = "bearer", // must be lower case
        BearerFormat = "JWT",
        Reference = new OpenApiReference
        {
            Id = JwtBearerDefaults.AuthenticationScheme,
            Type = ReferenceType.SecurityScheme
        }
    };
    options.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {securityScheme, new string[] { }}
    });
});

By the way, you can also change the default Swagger Doc description as below

options.SwaggerDoc("v1", new OpenApiInfo
{
    Version = "v1",
    Title = "MyDemo API",
    Description = "My Demo API by Winson from CoderBlog.in",
    TermsOfService = new Uri("https://www.coderblog.in"),
    Contact = new OpenApiContact
    {
        Name = "Contact",
        Url = new Uri("https://www.coderblog.in")
    },
    License = new OpenApiLicense
    {
        Name = "Example License",
        Url = new Uri("https://example.com/license")
    }
});

3. Find the JWT and use it

After that, restart the website, you will find there is an Authorize button in the Swagger UI.

Figure 1

After clicking this button, it will pop up to let you input the JWT value

Figure 2

Maybe you will ask how can I find the JWT token? All right, you can login to the website in frontend (I am still using this demo site), after login in Chrome, you can find the JWT in the Application tab, and find the local storage

Figure 3

And then input the JWT in Swagger, and you will find that will be a login status.

Figure 4

Also, you can find the lock is changed.

Figure 5

Now, you can try and access your API as well! 🙂

And if you change the Swagger Doc, you will also can find that the Swagger Doc has been changed

Figure 6

4. Conclusion

Swagger is very helpful for testing the API and finding the bugs, but if we use the JWT token then need to do the above settings to allow the Swagger to support it, this can save much of our time for the API testing!~

Loading

<p>The post How to support the JWT in Swagger UI first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2023/09/how-to-support-the-jwt-in-swagger-ui/feed/ 9
How to create and use the custom Middleware in .Net Core https://www.coderblog.in/2023/09/how-to-create-and-use-the-custom-middleware-in-net-core/ https://www.coderblog.in/2023/09/how-to-create-and-use-the-custom-middleware-in-net-core/#comments Tue, 05 Sep 2023 07:24:08 +0000 https://www.coderblog.in/?p=1078 1. Introduction Middleware in ASP.NET Core refers to software components that are assembled into an application pipeline to

<p>The post How to create and use the custom Middleware in .Net Core first appeared on Coder Blog.</p>

]]>
1. Introduction

Middleware in ASP.NET Core refers to software components that are assembled into an application pipeline to handle requests and responses. They are configured as part of the request processing pipeline in the Startup.cs file. Middleware can help separate cross-cutting concerns from app logic for modular and maintainable code, so it is good for logging, authentication, caching, compression, security, and more.

ASP.NET Core includes many built-in middleware like Static File Middleware, Routing Middleware, Endpoint Routing Middleware, etc. But in this article, we will create our middleware

2. Create and use the Middleware

For creating a middleware, there are two things that need to be done:

1) Create a constructor to inject the RequestDelegate for the next middleware
2) Implement the InvokeAsync method, pass the business object in it, and then you can handle everything that you want!

For example, we create a request log middleware to log each request, then we need to create a constructor as below

private readonly RequestDelegate _next;
public RequestLogMiddleware(RequestDelegate next)
{
    _next = next;
}

because we need to log the message, so it needs to pass the logger object in InvokeAsync, and define the logger in global

private readonly ILogger<RequestLogMiddleware> _logger;

public async Task InvokeAsync(HttpContext context, ILogger<RequestLogMiddleware> logger)
{
    logger.LogDebug("Request received: {Method} {Path}", context.Request.Method, context.Request.Path);
    await _next(context);
    logger.LogDebug("Response sent: {StatusCode}", context.Response.StatusCode);
}

So, the complete code is below

public class RequestLogMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLogMiddleware> _logger;

    public RequestLogMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, ILogger<RequestLogMiddleware> logger)
    {
        logger.LogDebug("Request received: {Method} {Path}", context.Request.Method, context.Request.Path);

        await _next(context);

        logger.LogDebug("Response sent: {StatusCode}", context.Response.StatusCode);
    }
}

after that, we can create an extension method for easy use it

public static class RequestLogMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLogMiddleware>();
    }
}

in the end, we can use the middleware in program.cs

var app = builder.Build();
app.UseRequestLogMiddleware();

3. Other Useful Middlewares

The middleware is easy to create and use, so I think the main point is what middleware (or ideas) can we create. I will show you some of them below 🙂

3.1. Error Handling Middleware

Handle the common errors

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            // Log error
            if (context.Response.StatusCode == 404)
            {
                // Redirect to 404 page
                context.Response.Redirect("/error/404");
            }
            else
            {
                // Handle other errors
                context.Response.StatusCode = 500;
                context.Response.ContentType = "text/plain";
                await context.Response.WriteAsync("An unexpected error occurred!");
            }
        }
    }
}

public static class ErrorHandlingMiddlewareExtensions
{
    public static IApplicationBuilder UseErrorHandlingMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ErrorHandlingMiddleware>();
    }
}

This middleware wraps all subsequent handlers in a try-catch block. If any unhandled exception occurs in later middleware, it will be caught here.

3.2. Response Compression Middleware

Compress responses to reduce bandwidth usage.

public class ResponseCompressionMiddleware
{
  private readonly RequestDelegate _next;

  public ResponseCompressionMiddleware(RequestDelegate next)
  {
    _next = next;
  }

  public async Task Invoke(HttpContext context)
  {
    // Check if client supports compression
    if(context.Request.Headers["Accept-Encoding"].Contains("gzip"))
    {
      // Stream to hold compressed response
      MemoryStream compressedStream = new MemoryStream(); 

      // Compress stream
      using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress))
      {
        await _next(context);
        context.Response.Body.CopyTo(gzipStream); 
      }

      // Replace uncompressed response with compressed one
      context.Response.Body = compressedStream;

      // Header to indicate compressed response
      context.Response.Headers.Add("Content-Encoding", "gzip"); 
    }
    else 
    {
      await _next(context);
    }
  }
}

public static class ResponseCompressionMiddlewareExtensions
{
    public static IApplicationBuilder UseResponseCompressionMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ResponseCompressionMiddleware>();
    }
}

This middleware checks the request headers, compresses the response using GZipStream if the client accepts gzip, and replaces the uncompressed response with the compressed one. This reduces bandwidth usage.

3.3. Async Middleware

Implementing asynchronous logic and database calls in a middleware using async/await.

public class AsyncMiddleware
{
  private readonly RequestDelegate _next;

  public AsyncMiddleware(RequestDelegate next)
  {
    _next = next;
  }

  public async Task Invoke(HttpContext context)
  {
    // Call database async
    var data = await FetchFromDatabase();

    // Network call
    var response = await CallExternalService();

    // Set context info
    context.Items["data"] = data;

    // Call next middleware
    await _next(context);
  } 

  private async Task<Data> FetchFromDatabase() 
  {
    // Fetch data from database
  }

  private async Task<Response> CallExternalService()
  {
   // Call API   
  }
}

public static class AsyncMiddlewareExtensions
{
    public static IApplicationBuilder UseAsyncMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<AsyncMiddleware>();
    }
}

This allows the middleware to perform asynchronous operations like database queries, network calls, long-running CPU tasks, etc. without blocking the request thread. The next middleware is invoked once the async work is completed.

3.4. Security Middleware

Verify user rights and role whether is correct

public class SecurityMiddleware 
{
  public async Task Invoke(HttpContext context)
  {
    // Check authentication
    if(!context.User.Identity.IsAuthenticated)
    {
      context.Response.Redirect("/account/login");
      return;
    }

    // Check authorization for admin page
    if(context.Request.Path.Value.StartsWith("/admin") && !context.User.HasRequiredRole("Admin")) 
    {
      context.Response.StatusCode = 403;
      return;
    }

    // Validate business rules
    if(!IsValidRequest(context.Request))
    {
       context.Response.StatusCode = 400;  
       return;
    }

    await _next(context);
  }

  private bool IsValidRequest(HttpRequest request)
  {
    // Check headers, params, business rules etc.
    return true;
  }
}

public static class SecurityMiddlewareExtensions
{
    public static IApplicationBuilder UseSecurityMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SecurityMiddleware>();
    }
}

This middleware encapsulates all security and validation logic into a single middleware that is applied globally. The app code only focuses on business logic.

3.5. Localization Middleware

Set culture, localization, or time zones based on request attributes.

public class LocalizationMiddleware
{
  public async Task Invoke(HttpContext context)
  {
    var userLanguage = context.Request.Headers["Accept-Language"].FirstOrDefault();

    // Set culture from header
    CultureInfo culture;
    if(!string.IsNullOrEmpty(userLanguage))
    {
       culture = new CultureInfo(userLanguage);
    }
    else 
    {
       culture = new CultureInfo("en-US"); 
    }

    CultureInfo.CurrentCulture = culture;
    CultureInfo.CurrentUICulture = culture;

    // Set timezone
    var timezone = context.Request.Headers["TimeZone"].FirstOrDefault();

    if(!string.IsNullOrEmpty(timezone))
    {
       TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timezone);
       TimeZoneInfo.Local = timeZone; 
    }

    await _next(context);
  }
}

public static class LocalizationMiddlewareExtensions
{
    public static IApplicationBuilder UseLocalizationMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<LocalizationMiddleware>();
    }
}

This middleware checks request headers for the user’s preferred language and timezone. It sets CurrentCulture and CurrentUICulture based on Accept-Language header. It also sets the Local TimeZone based on the TimeZone header. This can be used to dynamically localize responses for each user by looking up their headers. The app will format dates, numbers, and currencies appropriately for the user. The middleware must be configured early in program.cs before localizable content is generated.

3.6. Session Middleware

Manipulate session state and manage cookies

public class SessionMiddleware 
{
  public async Task Invoke(HttpContext context)
  {
    // Get session object
    var session = context.Session;

    // Set value in session
    session.SetString("Key", "Value");

    // Get value from session
    var value = session.GetString("Key");

    // Create cookie
    context.Response.Cookies.Append("CookieName", "CookieValue");

    // Delete cookie
    context.Response.Cookies.Delete("CookieName");

    // Set cookie expiration
    context.Response.Cookies.Append("CookieName", "Value", new CookieOptions
    {
      Expires = DateTime.Now.AddDays(1)
    });

    await _next(context);
  }
}

public static class SessionMiddlewareExtensions
{
    public static IApplicationBuilder UseSessionMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SessionMiddleware>();
    }
}

Session state and cookies are often used for user data, preferences, shopping carts, tokens etc. that need to persist across multiple requests. This middleware provides a centralized place to manage both sessions and cookies in your application.

3.7. RateLimit Middleware

A middleware that limits the number of requests from a client in a time period to prevent abuse/DDoS. It would keep track of requests and respond with 429 Too Many Requests if the limit exceeds.

 public class RateLimitMiddleware
{
  private const int PerMinuteLimit = 10;
  private static Dictionary<string, int> _requests = new Dictionary<string, int>();

  public async Task Invoke(HttpContext context)
  {
    var clientIp = context.Connection.RemoteIpAddress.ToString();

    // Increment counter for client
    if(_requests.ContainsKey(clientIp))
    {
      _requests[clientIp]++;  
    }
    else
    {
      _requests.Add(clientIp, 1);
    }

    // Check if over limit
    if(_requests[clientIp] > PerMinuteLimit)
    {
      context.Response.StatusCode = 429; // Too many requests
      return;
    }

    await _next.Invoke(context);

    // Remove counter after request is complete
    _requests.Remove(clientIp); 
  }
}

public static class RateLimitMiddlewareExtensions
{
    public static IApplicationBuilder UseRateLimitMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RateLimitMiddleware>();
    }
}

This allows limiting total requests per minute from a client IP. The limit and time window can be configured as per your needs.

3.8. URL Rewrite Middleware

Handle URL rewriting

public class RewriteMiddleware
{
  public async Task Invoke(HttpContext context)
  {
    var requestPath = context.Request.Path.Value;

    if(requestPath.StartsWith("/old"))
    {
      var newPath = requestPath.Replace("/old", "/new");

      context.Response.Redirect(newPath);
      return;
    }

    await _next(context);
  }
}

public static class RewriteMiddlewareExtensions
{
    public static IApplicationBuilder UseRewriteMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RewriteMiddleware>();
    }
}

This middleware checks the request path – if it starts with /old, it replaces that part with /new and redirects to the new URL.

For example, a request to /old/page will be redirected to /new/page.

The old to new URL mapping can be extended as:

var rewrites = new Dictionary<string, string>
{
  {"/old/page1", "/new/page1"},
  {"/old/page2", "/new/page2"}
};

var newPath = rewrites[requestPath];

This allows you to centrally handle URL rewrites for your entire application in a single place. Useful when reorganizing URLs.

The middleware can be placed early in the pipeline to redirect old URLs before the request goes further.

3.9. Https Redirect Middleware

Redirect all HTTP requests to https to enforce site-wide SSL.

public class HttpsRedirectMiddleware 
{
  public async Task Invoke(HttpContext context)
  {
    if (!context.Request.IsHttps)
    {
      var host = context.Request.Host.ToString();
      var redirectUrl = "https://" + host + context.Request.Path;

      context.Response.Redirect(redirectUrl); 
      return;
    }

    await _next(context);
  }
}

public static class HttpsRedirectMiddlewareExtensions
{
    public static IApplicationBuilder UseHttpsRedirectMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HttpsRedirectMiddleware>();
    }
}

This middleware checks if the request scheme is HTTPS. If not, it reconstructs the URL with an HTTPS scheme and redirects to it.

For example, a request to http://example.com/page will be redirected to https://example.com/page

This can be used to enforce HTTPS and SSL for your entire application by adding this middleware early in the pipeline.

3.10. Header injection Middleware

A middleware that injects useful headers like X-Request-Id for tracing requests across microservices.

public class HeaderInjectionMiddleware
{
  public async Task Invoke(HttpContext context)
  {
    // Generate unique request ID
    var requestId = Guid.NewGuid().ToString();

    // Inject X-Request-ID header
    context.Response.Headers.Add("X-Request-ID", requestId);

    await _next(context);
  }
}

public static class HeaderInjectionMiddlewareExtensions
{
    public static IApplicationBuilder UseHeaderInjectionMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HeaderInjectionMiddleware>();
    }
}

This middleware generates a unique GUID on each request and inserts it as a custom header X-Request-ID in the response.

This can be useful to:

1) Track and correlate requests across microservices
2) Debug issues by tracing logs with request ID
3) Analyze metrics for requests

This middleware can be configured early in the pipeline before headers are sent. Multiple headers can be injected in a similar way.

4. Conclusion

The middleware is useful and very easy to create, I shared some ideas on how to create and use it in this article, hope can help you solve the problems 🙂

Loading

<p>The post How to create and use the custom Middleware in .Net Core first appeared on Coder Blog.</p>

]]>
https://www.coderblog.in/2023/09/how-to-create-and-use-the-custom-middleware-in-net-core/feed/ 7