Back to "ASP=.\NET 核心的 EF Core 使用 HTMX @&EF 简单搜索"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET Entity Framework HTMX Postgres

ASP=.\NET 核心的 EF Core 使用 HTMX @&EF 简单搜索

Tuesday, 17 September 2024

一. 导言导言 导言 导言 导言

这只是一篇简短的文章,因为它以全文搜索系列中的其他文章为基础。 字头下调 和 Postgres 全文搜索. 我将展示您如何使用 HTMX 和 EF Core 在 ASP @.NET 核心应用程序中执行一个简单的搜索页面@.}

Don'我是不是已经搜索过了?

嗯,是的,}(在站点信头中) 我有一个搜索功能 提供打字标题 @(}当您输入结果时 即刻到达@).}然而我用移动模式隐藏了这一点 我还想将搜索结果连接起来 {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}@)_@ to a specast page .}这让用户体验更好,

搜索服务处

为了做到这一点,我改变了我的搜索方法 . 我创建了一个 BlogSearchService@ , @% 这是基于我的两种完整文本查询方法@.}这些不幸需要分为两个方法, EF.Functions.WebSearchToTsQuery("english", processedQuery) 和 EF.Functions.ToTsQuery("english", query + ":*").

第一组采用适当的搜索条件,第二组使用通配符搜索@.

    private IQueryable<BlogPostEntity> QueryForSpaces(string processedQuery)
    {
        return context.BlogPosts
            .Include(x => x.Categories)
            .Include(x => x.LanguageEntity)
            .AsNoTrackingWithIdentityResolution()
            .Where(x =>
                // Search using the precomputed SearchVector
                (x.SearchVector.Matches(EF.Functions.WebSearchToTsQuery("english",
                     processedQuery)) // Use precomputed SearchVector for title and content
                 || x.Categories.Any(c =>
                     EF.Functions.ToTsVector("english", c.Name)
                         .Matches(EF.Functions.WebSearchToTsQuery("english", processedQuery)))) // Search in categories
                && x.LanguageEntity.Name == "en") // Filter by language
            .OrderByDescending(x =>
                // Rank based on the precomputed SearchVector
                x.SearchVector.Rank(EF.Functions.WebSearchToTsQuery("english",
                    processedQuery)));
    }

    private IQueryable<BlogPostEntity> QueryForWildCard(string query)
    {
        return context.BlogPosts
            .Include(x => x.Categories)
            .Include(x => x.LanguageEntity)
            .AsNoTrackingWithIdentityResolution()
            .Where(x =>
                // Search using the precomputed SearchVector
                (x.SearchVector.Matches(EF.Functions.ToTsQuery("english",
                     query + ":*")) // Use precomputed SearchVector for title and content
                 || x.Categories.Any(c =>
                     EF.Functions.ToTsVector("english", c.Name)
                         .Matches(EF.Functions.ToTsQuery("english", query + ":*")))) // Search in categories
                && x.LanguageEntity.Name == "en") // Filter by language
            .OrderByDescending(x =>
                // Rank based on the precomputed SearchVector
                x.SearchVector.Rank(EF.Functions.ToTsQuery("english",
                    query + ":*"))); // Use precomputed SearchVector for ranking
    }

这些又用我的预数 SearchVector 栏,该列更新后创建并更新@ . @% 此创建于 DbContext 使用 OnModelCreating 方法@.

      entity.Property(b => b.SearchVector)
                .HasComputedColumnSql("to_tsvector('english', coalesce(\"Title\", '') || ' ' || coalesce(\"PlainTextContent\", ''))", stored: true);
            
           entity.HasIndex(b => b.SearchVector)
                .HasMethod("GIN");

这个方法的另一个缺点是,它只对英语有效 as @-iss.}我需要彻底重建数据库 让它为其他语言工作

然后我用这些方法 BlogSearchService 返回基于搜索查询的检查结果@. @%

    public async Task<PostListViewModel> GetPosts(string? query, int page = 1, int pageSize = 10)
    {
        if(string.IsNullOrEmpty(query))
        {
            return new PostListViewModel();
        }
        IQueryable<BlogPostEntity> blogPostQuery = query.Contains(" ") ? QueryForSpaces(query) : QueryForWildCard(query);
        var totalPosts = await blogPostQuery.CountAsync();
        var results = await blogPostQuery
            .Select(x => x.ToListModel())
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
        
        return new PostListViewModel()
        {
            Posts = results,
            TotalItems = totalPosts,
            Page = page,
            PageSize = pageSize
        };
        
    }

我使用简单的检查来确定查询中是否有空格, 以确定调用“ .” 的方法@ title

搜索控制器

搜索控制器遵循我对大多数我的控制器所使用的模式, 它检测调用是否来自 HTMX 以便发送部分或完整的布局页面 @ (@ 意指它用于直接导航以及HTMX请求\ MS K1\

[Route("search")]
public class SearchController(
    BaseControllerService baseControllerService,
    BlogSearchService searchService,
    ILogger<SearchController> logger)
    : BaseController(baseControllerService, logger)
{
    [HttpGet]
    [Route("{query?}")]
    public async Task<IActionResult> Search([FromRoute] string? query)
    {
        var searchResults = await searchService.GetPosts(query);
        var searchModel = new SearchResultsModel
        {
            Query = query,
            SearchResults = searchResults
        };
        searchModel = await PopulateBaseModel(searchModel);
        searchModel.SearchResults.LinkUrl = Url.Action("SearchResults", "Search");
        if (Request.IsHtmx()) return PartialView("SearchResults", searchModel);
        return View("SearchResults", searchModel);
    }

    [HttpGet]
    [Route("results")]
    public async Task<IActionResult> SearchResults([Required] string query, int page = 1, int pageSize = 10)
    {
        var searchResults = await searchService.GetPosts(query, page, pageSize);
        var searchModel = new SearchResultsModel
        {
            Query = query,
            SearchResults = searchResults
        };
        searchModel = await PopulateBaseModel(searchModel);
        searchModel.SearchResults.LinkUrl = Url.Action("SearchResults", "Search");
        if (Request.IsHtmx()) return PartialView("_SearchResultsPartial", searchModel.SearchResults);
        return View("SearchResults", searchModel);
    }
}

这是整个控制器@ ,} 您可以看到我有两个动作\ ,}一个返回页面 MS K2 可选包含结果的 page )}和一个只返回 HTMX 请求的结果\ MPK4}

 if (Request.IsHtmx()) return PartialView("_SearchResultsPartial", searchModel.SearchResults);

搜索结果部分

您可以看到此可选返回 _SearchResultsPartial 查看请求是否为 HTMX 结果请求@. @%

这是一个非常简单的 Paritial 视图, 它有传呼比特和结果@. @

@model Mostlylucid.Models.Blog.PostListViewModel
<div class="pt-2" id="content">
    @if (Model.Posts?.Any() is true)
    {
        <div class="inline-flex w-full items-center justify-center print:!hidden">
            @if (Model.TotalItems > Model.PageSize)
            {
                <pager
                    x-ref="pager"
                    link-url="@Model.LinkUrl"
                    hx-boost="true"
                    hx-target="#content"
                    hx-swap="show:none"
                    page="@Model.Page"
                    page-size="@Model.PageSize"
                    total-items="@Model.TotalItems"
                    hx-headers='{"pagerequest": "true"}'>
                </pager>
            }
            <partial name="_Pager" model="Model"/>

        </div>
        @foreach (var post in Model.Posts)
        {
            <partial name="_ListPost" model="post"/>
        }
    }
</div>

列表部分视图

我用同样的 _ListPost 偏移视图, 只要我需要列出位置@. @%

@model Mostlylucid.Models.Blog.PostListModel

<div class="border-b border-grey-lighter pb-8 mb-8">
 
    <a asp-controller="Blog" asp-action="Show" hx-boost="true"  hx-swap="show:window:top"  hx-target="#contentcontainer" asp-route-slug="@Model.Slug"
       class="block font-body text-lg font-semibold transition-colors hover:text-green text-blue-dark dark:text-white  dark:hover:text-secondary">@Model.Title</a>  
    <div class="flex flex-wrap space-x-2 items-center py-4 print:!hidden">
    @foreach (var category in Model.Categories)
    {
        <partial name="_Category" model="category"/>
    }
    @{ var languageModel = (Model.Slug, Model.Languages, Model.Language); }
        <partial name="_LanguageList" model="languageModel"/>
    </div>
    <div class="block font-body text-black dark:text-white">@Model.Summary</div>
    <div class="flex items-center pt-4">
        <p class="pr-2 font-body font-light text-primary light:text-black dark:text-white">
            @Model.PublishedDate.ToString("f")
        </p>
        <span class="font-body text-grey dark:text-white">//</span>
        <p class="pl-2 font-body font-light text-primary light:text-black dark:text-white">
            @Model.ReadingTime
        </p>
    </div>
</div>

HTMX和搜索页面

我在此使用 HTMX 很简单@. @I just hook into the button {(} 这一次我决定不更改按键上输入的\ /}修改 URL)}, 当点击按钮时发送请求 *%.} I 在请求中包含查询使用 hx-include 目标 #content div 替换结果@. @%

<div class="flex items-center gap-2 bg-neutral-500 bg-opacity-10 p-2 rounded-lg">
    <button
        hx-get="@Url.Action("SearchResults", "Search")"
        hx-target="#content"
        hx-include="[name='query']"
        hx-swap="outerHTML"
        class="btn btn-outline btn-sm flex items-center gap-2 text-black dark:text-white">
        Search
        <i class="bx bx-search text-lg"></i>
    </button>
    <input
        type="text"
        placeholder="Search..."
        value="@Model.Query"
        name="query"
        class="input input-sm border-0 grow text-black dark:text-white bg-transparent focus:outline-none"/>

</div>

最新更新

因此,继一些评论之后 哈立德 我决定加强此功能, 使搜索成为#:}

  1. 在 Enter 键按键触发
  2. 输入两个以上的字符后触发“% ( @ @ oso” 变成一个松散的字头@ MS K1

未来我需要将页面大小功能重新添加到“;”中,

更新搜索表单

要做到这一点,我首先将输入以窗体包装,然后使用 Alpine.js 在用户键入@.}时提交表格。 你可以看到我用的是 x-data 来为查询创建一个活性变量,然后我检查查询的长度以确定是否提交窗体@.

<form
    x-data="{ query: '@Model.Query', checkSubmit() { if (this.query.length > 2) { $refs.searchButton.click(); } } }"
    class="flex items-center gap-2 bg-neutral-500 bg-opacity-10 p-2 rounded-lg"
    action="@Url.Action("Search", "Search")"
    hx-push-url="true"
    hx-boost="true"
    hx-target="#content"
    hx-swap="outerHTML show:window:top"
    hx-headers='{"pagerequest": "true"}'>
    <button
        type="submit"
        x-ref="searchButton"
        class="btn btn-outline btn-sm flex items-center gap-2 text-black dark:text-white">
        Search
        <i class="bx bx-search text-lg"></i>
    </button>
    <input
        type="text"
        placeholder="Search..."
        name="query"
        value="@Model.Query"
        x-model="query"
        x-on:input.debounce.200ms="checkSubmit"
        x-on:keydown.enter.prevent="$refs.searchButton.click()"
        class="input input-sm border-0 grow text-black dark:text-white bg-transparent focus:outline-none"
    />
</form>

为了重新使用同样的控制器动作,我还设定了 pagerequest 显示此请求的页眉@ . @ action

我也使用阿尔卑斯山 x-on:keydown.enter.prevent 在按下 Enter 键时单击按钮,并调出输入以阻止太多请求@. @%

主计长最新情况

在控制器中, 我删除了搜索Results 动作, 相反在主控件上添加更多的 @ ' intelligence@ MS K1 Search 用于处理初始搜索和被忽略请求的动作@. @%

在这里您可以看到我加了一个额外的参数 pagerequest 来确定此请求是否为附加请求, 并显示应该从信头收藏中填入该请求@. @ @ @ action


    [HttpGet]
    [Route("")]
    public async Task<IActionResult> Search(string? query, int page = 1, int pageSize = 10,[FromHeader] bool pagerequest=false)
    {
        var searchResults = await searchService.GetPosts(query, page, pageSize);
        var searchModel = new SearchResultsModel
        {
            Query = query,
            SearchResults = searchResults
        };
        searchModel = await PopulateBaseModel(searchModel);
        var linkUrl = Url.Action("Search", "Search");
        searchModel.SearchResults.LinkUrl = linkUrl;
        if(pagerequest && Request.IsHtmx()) return PartialView("_SearchResultsPartial", searchModel.SearchResults);
        
        if (Request.IsHtmx()) return PartialView("SearchResults", searchModel);
        return View("SearchResults", searchModel);
    }

然后我就会得到结果 并检测到这个信头, 以确定哪个视图@ / @ partview 返回Q. {

我还添加了一个精细的退休者行动 来处理类似 /search/umami 以查询.重定向到主搜索页面

   [HttpGet]
    [Route("{query}")]
    public  IActionResult InitialSearch([FromRoute] string query)
    {
        return RedirectToAction("Search", new { query });
    }

在结论中的结论

这是使用 HTMX 和 EF Core 在 ASP @. @NET 核心应用程序@.}您可以轻松扩展此功能以包括更多功能, 如过滤{,}排序},}(或甚至与其他搜索服务整合).}关键取用是如何利用HTMX 为用户提供顺利的经验,同时保持后端逻辑的清洁和效率>.}

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.