2881099 / AdminBlazor

AdminBlazor 是一款 Blazor Server SaaS 后台管理项目,支持 RABC 权限菜单/按钮,支持一对一、一对多、多对多代码生成 .razor 界面。 集成功能:菜单、角色、用户、定时任务、数据字典、租户 依赖组件:BootstrapBlazor、FreeSql
Apache License 2.0
65 stars 16 forks source link

【讨论】对外提供访问接口的模板 #5

Open hd2y opened 7 months ago

hd2y commented 7 months ago

我这边开发时,生成的应用还需要提供出接口供前端访问,这里基于这个需求和聚合根的特点,提供一个案例供大家参考:

首先是分页查询的入参:

/// <summary>
/// 分页查询输入
/// </summary>
public class PagedInput
{
    /// <summary>
    /// 过滤条件
    /// </summary>
    public DynamicFilterInfo? Filter { get; set; }

    /// <summary>
    /// 页码
    /// </summary>
    public int Page { get; set; }

    /// <summary>
    /// 分页数
    /// </summary>
    public int Size { get; set; }

    /// <summary>
    /// 排序
    /// </summary>
    public string? OrderBy { get; set; }

    /// <summary>
    /// 是否正序
    /// </summary>
    public bool Asc { get; set; }

    /// <summary>
    /// 包含的导航属性,如果一对多关系,需要包含多级导航属性用逗号分隔
    /// </summary>
    public string[]? Include { get; set; }
}

返回内容就比较简单了:

/// <summary>
/// 分页查询结果
/// </summary>
public class PagedOutput<TEntity> where TEntity : class
{
    /// <summary>
    /// 总数
    /// </summary>
    public long Total { get; set; }

    /// <summary>
    /// 分页数据
    /// </summary>
    public List<TEntity> Data { get; set; }
}

然后控制器设置一个 EntityControllerBase 方便其他聚合根实体对外公开接口时继承:

public class EntityControllerBase<TEntity, TKey>(IAggregateRootRepository<TEntity> repository)
    where TEntity : class, IEntity<TKey>
{
    /// <summary>
    /// 查询
    /// </summary>
    [HttpGet]
    public Task<TEntity> GetAsync(TKey id, CancellationToken cancellationToken) =>
        repository.Select.Where(t => t.Id.Equals(id)).FirstAsync(cancellationToken);

    /// <summary>
    /// 分页查询
    /// </summary>
    [HttpPost]
    public Task<PagedOutput<TEntity>> GetPagedAsync([FromBody] PagedInput input, CancellationToken cancellationToken)
    {
        var select = repository.Select;

        if (input.Filter != null)
        {
            select = select.WhereDynamicFilter(input.Filter);
        }

        if (input.Include is { Length: > 0 })
        {
            foreach (var includeProperty in input.Include)
            {
                if (string.IsNullOrWhiteSpace(includeProperty)) continue;
                var includeProperties = includeProperty.Split(",");

                // includeProperties 长度不固定,循环构造表达式树获取内容
                Expression<Action<ISelect<object>>> exp = null;
                for (var i = includeProperties.Length - 1; i > 0; i--)
                {
                    // 创建一个表示输入参数的参数表达式
                    var parameter = Expression.Parameter(typeof(ISelect<object>), "then" + i);

                    if (exp == null)
                    {
                        // 获取 IncludeByPropertyName 方法的 MethodInfo
                        var methodInfo = typeof(ISelect<object>).GetMethod("IncludeByPropertyName", [typeof(string)]);
                        var propertyName = Expression.Constant(includeProperties[i]);
                        var methodCall = Expression.Call(parameter, methodInfo, propertyName);

                        // 创建一个带有输入参数和方法调用的 Lambda 表达式
                        exp = Expression.Lambda<Action<ISelect<object>>>(methodCall, parameter);
                    }
                    else
                    {
                        // 获取 IncludeByPropertyName 方法的 MethodInfo
                        var methodInfo = typeof(ISelect<object>).GetMethod("IncludeByPropertyName",
                            [typeof(string), typeof(Expression<Action<ISelect<object>>>)]);
                        var propertyName = Expression.Constant(includeProperties[i]);
                        var methodCall = Expression.Call(parameter, methodInfo, propertyName, exp);

                        // 创建一个带有输入参数和方法调用的 Lambda 表达式
                        exp = Expression.Lambda<Action<ISelect<object>>>(methodCall, parameter);
                    }
                }

                select = exp == null
                    ? select.IncludeByPropertyName(includeProperties[0])
                    : select.IncludeByPropertyName(includeProperties[0], exp);
            }
        }

        if (!string.IsNullOrEmpty(input.OrderBy))
        {
            select = select.OrderBy(input.OrderBy, input.Asc);
        }

        return select.Count(out var total)
            .Page(Math.Max(input.Page, 1), Math.Min(Math.Max(input.Size, 1), 1000))
            .ToListAsync(cancellationToken)
            .ContinueWith(t => new PagedOutput<TEntity>
            {
                Total = total,
                Data = t.Result
            }, cancellationToken);
    }

    /// <summary>
    /// 新增
    /// </summary>
    [HttpPost]
    public Task<TEntity> AddAsync([FromBody] TEntity entity, CancellationToken cancellationToken) =>
        repository.InsertAsync(entity, cancellationToken);

    /// <summary>
    /// 删除
    /// </summary>
    [HttpDelete]
    public Task<int> DeleteAsync(TKey id, CancellationToken cancellationToken) =>
        repository.DeleteAsync(t => t.Id.Equals(id), cancellationToken);

    /// <summary>
    /// 更新
    /// </summary>
    [HttpPost]
    public Task<int> UpdateAsync([FromBody] TEntity entity, CancellationToken cancellationToken)
        => repository.UpdateDiy.SetSource(entity).ExecuteAffrowsAsync(cancellationToken);

    /// <summary>
    /// 批量新增
    /// </summary>
    [HttpPost]
    public async Task<int> AddMultipleAsync([FromBody] TEntity[] entities, CancellationToken cancellationToken)
    {
        var count = 0;
        foreach (var entity in entities)
        {
            await repository.InsertAsync(entity, cancellationToken);
            count++;
        }

        return count;
    }

    /// <summary>
    /// 批量删除
    /// </summary>
    [HttpDelete]
    public Task<int> DeleteMultipleAsync(TKey[] id, CancellationToken cancellationToken) =>
        repository.DeleteAsync(t => id.Contains(t.Id), cancellationToken);

    /// <summary>
    /// 批量更新
    /// </summary>
    [HttpPost]
    public Task<int> UpdateMultipleAsync([FromBody] TEntity[] entities, CancellationToken cancellationToken) =>
        repository.UpdateDiy.SetSource(entities).ExecuteAffrowsAsync(cancellationToken);
}

例如我有个 Component 实体,对外公开接口,则创建一个 ComponentController 继承 EntityControllerBase:

[Route("api/[controller]/[action]")]
public class ComponentController(IAggregateRootRepository<Component> repository)
    : EntityControllerBase<Component, long>(repository)
{
}
hd2y commented 7 months ago

主要是分页查询拓展了一下 Include 属性,这里是分页查询 Form 表单的一个入参示例:

{
    "filter": {
        "logic": "And",
        "filters": [
            {
                "field": "id",
                "operator": "GreaterThanOrEqual",
                "value": 1
            }
        ]
    },
    "page": 1,
    "size": 10,
    "orderBy": "id",
    "asc": true,
    "include": [
        "formGroups",
        "formGroups,warehouse",
        "formGroups,warehouse.project",
        "formItems",
        "formItems,formItemProps",
        "formItems,formItemProps,componentProp",
        "formItems,formItemProps,componentProp.component"
    ]
}
2881099 commented 7 months ago

有想法可以直接PR进来