EF Core SQL Server 添加视图映射

最近的项目(后端:.Net Core Web API 、数据库: SQL Server、前端:Blazor WebAssembly、前端Ui框架:Ant Design Blazor)中有些列表页面需要从后端拿到多表查询的结果,一种做法是从后端多个接口分别拿到数据在前端进行整合,比如:

@page "/orders"

@inject IDictService DictService
@inject IUserService UserService
@inject IOrderService OrderService
@inject IProductService ProductService
@inject ICustomerService CustomerService
@inject NavigationManager NavigationManager

@implements IDisposable

@attribute [Authorize]

<PageTitle>全部订单</PageTitle>

<Breadcrumb Style="margin:16px 0;">
    <BreadcrumbItem>订单</BreadcrumbItem>
    <BreadcrumbItem>全部订单</BreadcrumbItem>
</Breadcrumb>

<div class="site-layout-background" style="padding: 24px;">
    <GridRow>
        <GridCol Span="24">
            <Table TItem="OrderMainDTO" DataSource="@orders" @bind-PageSize="pageSize" ScrollX="1300px">
                <ChildContent>
                    <PropertyColumn Property="c=>c.OrderRequestNo" Title="订单申请编号" Sortable Filterable>
                        @context.OrderRequestNo
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.OrderCode" Title="外部订单编号" Sortable Filterable>
                        @context.OrderCode
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.CustomerId" Title="客户" Sortable Filterable>
                        <Template>
                            @{
                                var customer = CustomerService.Customers.FirstOrDefault(c => c.Id == context.CustomerId);
                                if (customer != null)
                                {
                                    @customer.CustomerName
                                }
                            }
                        </Template>
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.IndustryId" Title="行业" Sortable Filterable>
                        <Template>
                            @{
                                var industry = ProductService.Industries.FirstOrDefault(c => c.Id == context.IndustryId);
                                if (industry != null)
                                {
                                    @industry.IndustryName
                                }
                            }
                        </Template>
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.ProductLineId" Title="产品系列" Sortable Filterable>
                        <Template>
                            @{
                                var productLine = ProductService.ProductLines.FirstOrDefault(c => c.Id == context.ProductLineId);
                                if (productLine != null)
                                {
                                    @productLine.ProductLineName
                                }
                            }
                        </Template>
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.SalesOwnerId" Title="销售负责人" Sortable Filterable>
                        <Template>
                            @{
                                var salesOwner = UserService.Employees.FirstOrDefault(c => c.Id == context.SalesOwnerId);
                                if (salesOwner != null)
                                {
                                    @salesOwner.FullName
                                }
                            }
                        </Template>
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.ProductLineOwnerId" Title="产品负责人" Sortable Filterable>
                        <Template>
                            @{
                                var productLineOwner = UserService.Employees.FirstOrDefault(c => c.Id == context.ProductLineOwnerId);
                                if (productLineOwner != null)
                                {
                                    @productLineOwner.FullName
                                }
                            }
                        </Template>
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.ApprovalStatus" Title="审批状态" Sortable Filterable>
                        @{
                            var color = "";
                            var approvalTag = "";
                            @switch (context.ApprovalStatus)
                            {
                                case 0:
                                    approvalTag = "草稿";
                                    color = "orange";
                                    break;
                                case 1:
                                    approvalTag = "审批中";
                                    color = "purple";
                                    break;
                                case 2:
                                    approvalTag = "审批完成";
                                    color = "green";
                                    break;
                                case 3:
                                    approvalTag = "拒绝";
                                    color = "red";
                                    break;
                                case 4:
                                    approvalTag = "退回";
                                    color = "magenta";
                                    break;
                                default:
                                    approvalTag = "无";
                                    color = "default";
                                    break;
                            }

                            <Tag Color="@color">
                                @approvalTag
                            </Tag>
                        }
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.ExecutingStatus" Title="执行状态" Sortable Filterable>
                        @{
                            var color = "";
                            var executingTag = "";

                            @switch (context.ExecutingStatus)
                            {
                                case 0:
                                    executingTag = "未执行";
                                    color = "orange";
                                    break;
                                case 1:
                                    executingTag = "执行中";
                                    color = "geekblue";
                                    break;
                                case 2:
                                    executingTag = "执行完成";
                                    color = "green";
                                    break;
                                default:
                                    executingTag = "无";
                                    color = "default";
                                    break;
                            }

                            <Tag Color="@color">
                                @executingTag
                            </Tag>
                        }
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.SubmitterId" Title="提交人" Sortable Filterable>
                        <Template>
                            @{
                                var submitter = UserService.Employees.FirstOrDefault(c => c.Id == context.SubmitterId);
                                if (submitter != null)
                                {
                                    @submitter.FullName
                                }
                            }
                        </Template>
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.CreateTime" Title="提交时间" Format="yyyy-MM-dd hh:mm:ss" Sortable Filterable></PropertyColumn>
                     <ActionColumn Title="操作" Width="100" Fixed="right">
                         <Tooltip Title="查看">
                             <Button Shape="@ButtonShape.Circle" Icon="@IconType.Outline.Eye" OnClick="() => GoToOrder(context.Id)" />
                         </Tooltip>
                     </ActionColumn>
                 </ChildContent>
                 <PaginationTemplate>
                     <Pagination Class="@(context.PaginationClass + " my-custom-pagination")"
                                 Total="context.Total"
                                 PageSize="context.PageSize"
                                 Current="context.PageIndex"
                                        ShowSizeChanger
                                 OnChange="context.HandlePageChange" />
                 </PaginationTemplate>
             </Table>
         </GridCol>
     </GridRow>
 </div>

 @code {
    List<OrderMainDTO> orders = new();
    IEnumerable<Dict> dicts = [];
    int total => orders.Count();
    int pageSize = 20;

    void GoToOrder(Guid orderId)
    {
        NavigationManager.NavigateTo($"/orders/detail/{orderId}");
    }

    protected override async Task OnInitializedAsync()
    {
        var dictResponse = await DictService.GetDicts();
        if (dictResponse.Success)
        {
            dicts = dictResponse.Data?.Count > 0 ? dictResponse.Data : [];
        }

        // 获取所有行业数据
        await ProductService.GetIndustries();
        // 获取所有产品线数据
        await ProductService.GetProductLines();
        // 获取所有产品
        await ProductService.GetProducts();
        // 获取所有客户数据
        await CustomerService.GetCustomers();
        // 获取所有用户数据
        await UserService.GetEmployees();

        var result = await OrderService.GetOrders();
        if (result.Success)
        {
            orders = result.Data != null ? result.Data : [];
        }

        ProductService.OnChange += StateHasChanged;
        CustomerService.OnChange += StateHasChanged;
        UserService.OnChange += StateHasChanged;
    }

    public void Dispose()
    {
        ProductService.OnChange -= StateHasChanged;
        CustomerService.OnChange -= StateHasChanged;
        UserService.OnChange -= StateHasChanged;
    }
}

从这个Blazor WebAssembly 组件中可以看到为了实现一个列表分别调用了:
行业数据、产品线数据、客户数据、用户数据、字典数据等接口,并且在Table中进行适配,这种方式虽然能实现这个订单列表的页面,然而产生了一个和UI框架的冲突,如下图:

Ant Design Blazor 的 Table 带有过滤器(Filterable),由于 Table 中的各列是遍历的原始值,再通过适配转成了文本,这直接导致过滤器作用的是未适配前的原始值,比如截图中过滤器它应该过滤审批状态,你填入“审批完成”再点确定是查不到任何结果的,审批完成对应的是2,如果你输入2再查询是可以有结果的。

订单列表直接使用视图数据可以减少调用接口的数量也顺带解决掉Table过滤器的这个冲突,EF Core 如何映射视图呢?从微软的官方文档通过关键字没找到合适的内容,搜索引擎上找了一会儿拿到的内容一尝试还是错的,花了一些时间踩坑和试错。

  • 数据库中新建好视图
  • 后端新建一个用于适配该视图的模型
  • DbContext 数据库上下文中新增视图
  • 服务和Controller中修改数据库操作和查询返回的类型
  • 前端接口请求服务中类型修改
  • 前端组件中重新适配新接口以及绑定

新建视图-下列SQL Case When 遇到中文需要在引号前加N,否则会乱码

SELECT 
O.Id,
O.OrderRequestNo,
O.OrderCode,
C.CustomerName,
I.IndustryName,
PL.ProductLineName,
O.SalesOwnerId,
U1.FullName AS SalesOwner,
O.SubmitterId,
U3.FullName AS Submitter,
O.ProductLineOwnerId,
U2.FullName AS ProductLineOwner,
O.ApprovalStatus,
CASE
	WHEN O.ApprovalStatus = 0 THEN N'草稿'
	WHEN O.ApprovalStatus = 1 THEN N'审批中'
	WHEN O.ApprovalStatus = 2 THEN N'审批完成'
	WHEN O.ApprovalStatus = 3 THEN N'拒绝'
	WHEN O.ApprovalStatus = 4 THEN N'退回'
END AS ApprovalStatusName,
O.ExecutingStatus,
CASE
	WHEN O.ExecutingStatus = 0 THEN N'未执行'
	WHEN O.ExecutingStatus = 1 THEN N'执行中'
	WHEN O.ExecutingStatus = 2 THEN N'执行完成'
END AS ExecutingStatusName,
O.CreateTime,
O.IsDisabled
FROM [dbo].[T_OrderMains] AS O 
LEFT JOIN [dbo].[T_Industries] AS I ON O.IndustryId = I.Id
LEFT JOIN [dbo].[T_ProductLines] AS PL ON PL.Id = O.ProductLineId
LEFT JOIN [dbo].[T_Users] AS U1 ON U1.Id = O.SalesOwnerId
LEFT JOIN [dbo].[T_Users] AS U2 ON U2.Id = O.ProductLineOwnerId
LEFT JOIN [dbo].[T_Users] AS U3 ON U3.Id = O.SubmitterId
LEFT JOIN [dbo].[T_Customers] AS C ON C.Id = O.CustomerId

新建模型

namespace Gatcis.Plus.Shared.Models.OrderModel
{
    public class OrderView
    {
        public Guid Id { get; set; }
        public string? OrderRequestNo { get; set; }
        public string? OrderCode { get; set; }
        public string? CustomerName { get; set; }
        public string? IndustryName { get; set; }
        public string? ProductLineName { get; set; }
        public Guid SalesOwnerId { get; set; }
        public string? SalesOwner { get; set; }
        public Guid SubmitterId { get; set; }
        public string? Submitter { get; set; }
        public Guid ProductLineOwnerId { get; set; }
        public string? ProductLineOwner { get; set; }
        public int ApprovalStatus { get; set; }
        public string? ApprovalStatusName { get; set; }
        public int ExecutingStatus { get; set; }
        public string? ExecutingStatusName { get;set; }
        public DateTime CreateTime { get; set; }
        public bool IsDisabled { get; set; }
    }
}

DbContext 中新增视图-这样去映射视图是不需要进行 Migration (数据库迁移)的

namespace Gatcis.Plus.Service.Data
{
    public class IDbContext: IdentityDbContext<IUser, IRole, Guid>
    {
        // ... 其他
        // 订单视图
        public DbSet<OrderView> View_OrderLists { get; set; }
        public IDbContext(DbContextOptions<IDbContext> options) : base(options) { }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<OrderView>().ToView("View_OrderLists");
        }
    }
}

修改后端接口服务

        public async Task<ServiceResponse<List<OrderView>>> GetMyOrders(Guid operatorId)
        {
            var response = new ServiceResponse<List<OrderView>>();
            var orders = await context.View_OrderLists
                .Where(o => o.IsDisabled == false && (o.ProductLineOwnerId == operatorId || o.SalesOwnerId == operatorId || o.SubmitterId == operatorId))
                .OrderByDescending(o => o.CreateTime)
                .ToListAsync();

            response.Code = 200;
            response.Data = orders;

            return response;
        }

最终前端页面适配

@page "/orders/my"

@inject IOrderService OrderService
@inject NavigationManager NavigationManager

@attribute [Authorize]

<PageTitle>我的订单</PageTitle>

<Breadcrumb Style="margin:16px 0;">
    <BreadcrumbItem>订单</BreadcrumbItem>
    <BreadcrumbItem>我的订单</BreadcrumbItem>
</Breadcrumb>

<div class="site-layout-background" style="padding: 24px;">
    <GridRow>
        <GridCol Span="24">
            <Table TItem="OrderView" DataSource="@orders" @bind-PageSize="pageSize" ScrollX="1300px">
                <ChildContent>
                    <PropertyColumn Property="c=>c.OrderRequestNo" Title="订单申请编号" Sortable Filterable>
                        @context.OrderRequestNo
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.OrderCode" Title="外部订单编号" Sortable Filterable>
                        @context.OrderCode
                    </PropertyColumn>
                    <PropertyColumn  Property="c=>c.CustomerName" Title="客户" Sortable Filterable>
                        @context.CustomerName
                    </PropertyColumn>
                     <PropertyColumn Property="c=>c.IndustryName" Title="行业" Sortable Filterable>
                        @context.IndustryName
                     </PropertyColumn>
                     <PropertyColumn Property="c=>c.ProductLineName" Title="产品系列" Sortable Filterable>
                        @context.ProductLineName
                     </PropertyColumn>
                     <PropertyColumn Property="c=>c.SalesOwner" Title="销售负责人" Sortable Filterable>
                        @context.SalesOwner
                     </PropertyColumn>
                     <PropertyColumn Property="c=>c.ProductLineOwner" Title="产品负责人" Sortable Filterable>
                        @context.ProductLineOwner
                     </PropertyColumn>
                    <PropertyColumn Property="c=>c.ApprovalStatusName" Title="审批状态" Sortable Filterable>
                        @{
                            var color = "";
                            @switch (context.ApprovalStatus)
                            {
                                case 0:
                                    color = "orange";
                                    break;
                                case 1:
                                    color = "purple";
                                    break;
                                case 2:
                                    color = "green";
                                    break;
                                case 3:
                                    color = "red";
                                    break;
                                case 4:
                                    color = "magenta";
                                    break;
                            }

                            <Tag Color="@color">
                                @context.ApprovalStatusName
                            </Tag>
                        }
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.ExecutingStatusName" Title="执行状态" Sortable Filterable>
                        @{
                            var color = "";
                            @switch (context.ExecutingStatus)
                            {
                                case 0:
                                    color = "orange";
                                    break;
                                case 1:
                                    color = "geekblue";
                                    break;
                                case 2:
                                    color = "green";
                                    break;
                            }

                            <Tag Color="@color">
                                @context.ExecutingStatusName
                            </Tag>
                        }
                    </PropertyColumn>
                    <PropertyColumn Property="c=>c.Submitter" Title="提交人" Sortable Filterable>
                        @context.Submitter
                    </PropertyColumn>
                     <PropertyColumn Property="c=>c.CreateTime" Title="提交时间" Format="yyyy-MM-dd hh:mm:ss" Sortable Filterable></PropertyColumn>
                     <ActionColumn Title="操作" Width="100" Fixed="right">
                        @if (context.ApprovalStatus == 1 && context.ProductLineOwnerId == currentUserId)
                        {
                            <Tooltip Title="审批">
                                <Button Shape="@ButtonShape.Circle" Icon="@IconType.Outline.Check" OnClick="() => GoToOrder(context.Id)" />
                            </Tooltip>
                        }
                        else
                        {
                            <Tooltip Title="查看">
                                <Button Shape="@ButtonShape.Circle" Icon="@IconType.Outline.Eye" OnClick="() => GoToOrder(context.Id)" />
                            </Tooltip>
                        }
                     </ActionColumn>
                 </ChildContent>
                 <PaginationTemplate>
                     <Pagination Class="@(context.PaginationClass + " my-custom-pagination")"
                                 Total="context.Total"
                                 PageSize="context.PageSize"
                                 Current="context.PageIndex"
                                        ShowSizeChanger
                                 OnChange="context.HandlePageChange" />
                 </PaginationTemplate>
             </Table>
         </GridCol>
     </GridRow>
 </div>

 @code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthenticationState { get; set; }

    public ClaimsPrincipal? authenticatedUser { get; set; }
    Guid? currentUserId = null;

    List<OrderView> orders = new();

    int total => orders.Count();
    int pageSize = 20;

    void GoToOrder(Guid orderId)
    {
        NavigationManager.NavigateTo($"/orders/detail/{orderId}");
    }

    protected override async Task OnInitializedAsync()
    {
        var result = await OrderService.GetMyOrders();
        if (result.Success)
        {
            orders = result.Data != null ? result.Data : [];
        }

        if (AuthenticationState is not null)
        {
            var state = await AuthenticationState;
            authenticatedUser = state.User;

            string? userId = authenticatedUser.FindFirst(c => c.Type.Contains("nameidentifier"))?.Value;
            if (userId != null)
            {
                currentUserId = Guid.Parse(userId);
            }
        }
    }
}