首页 > 解决方案 > React-Admin 无法发布到 ASP.Net Core API

问题描述

我正在尝试将 ASP.NET Core 3 api 与 React-Admin 连接起来。到目前为止,我可以列出数据库中的条目并显示一条记录——显然这两种 GET 方法工作正常。(坚持这个例子

当我尝试使用 POST 方法创建记录时,我收到 400 Bad Request 并且无法跟踪问题。

我的 App.js 看起来像这样:

import simpleRestProvider from 'ra-data-simple-rest';
import realApi from './dataProvider/myDataProvider';

const dataProvider = simpleRestProvider('api/');

const App = () =>
    <Admin        
        dashboard={Dashboard}
        dataProvider={realApi}        
    >
        <Resource name={projects.basePath} {...projects.crud} />
        ...
    </Admin>;

我有从官方 react-admin 教程复制的自定义 dataProvider

    export default (type, resource, params) => {    
    let url = '';
    const options = { 
        headers: new Headers({
            Accept: 'application/json',
            "Content-Type": 'application/json; charset=utf-8',
        }),
    };

    let query = "";
    switch (type) {
        case GET_LIST: {
            const { page, perPage } = params.pagination;
            const { field, order } = params.sort;
            query = {
                sort: JSON.stringify([field, order]),
                range: JSON.stringify([
                    (page - 1) * perPage,
                    page * perPage - 1,
                ]),
                filter: JSON.stringify(params.filter),
            };
            url = `${apiUrl}/${resource}?${stringify(query)}`;
            break;
        }
        case GET_ONE:
            url = `${apiUrl}/${resource}/${params.id}`;
            break;
        case CREATE:
            url = `${apiUrl}/${resource}`;
            options.method = 'POST';
            options.body = JSON.stringify(params.data);
            break;
        case UPDATE:
            url = `${apiUrl}/${resource}/${params.id}`;
            options.method = 'PUT';
            options.body = JSON.stringify(params.data);
            break;
        case UPDATE_MANY:
            query = {
                filter: JSON.stringify({ id: params.ids }),
            };
            url = `${apiUrl}/${resource}?${stringify(query)}`;
            options.method = 'PATCH';
            options.body = JSON.stringify(params.data);
            break;
        case DELETE:
            url = `${apiUrl}/${resource}/${params.id}`;
            options.method = 'DELETE';
            break;
        case DELETE_MANY:
            query = {
                filter: JSON.stringify({ id: params.ids }),
            };
            url = `${apiUrl}/${resource}?${stringify(query)}`;
            options.method = 'DELETE';
            break;
        case GET_MANY: {
            query = {
                filter: JSON.stringify({ id: params.ids }),
            };
            url = `${apiUrl}/${resource}?${stringify(query)}`;
            break;
        }
        case GET_MANY_REFERENCE: {
            const { page, perPage } = params.pagination;
            const { field, order } = params.sort;
            query = {
                sort: JSON.stringify([field, order]),
                range: JSON.stringify([
                    (page - 1) * perPage,
                    page * perPage - 1,
                ]),
                filter: JSON.stringify({
                    ...params.filter,
                    [params.target]: params.id,
                }),
            };
            url = `${apiUrl}/${resource}?${stringify(query)}`;
            break;
        }
        default:
            throw new Error(`Unsupported Data Provider request type ${type}`);
    }


    let headers;
    return fetch(url, options)
        .then(res => {
            headers = res.headers;
            debugger
            return res.json();
        })
        .then(json => {
            switch (type) {
                case GET_LIST:
                case GET_MANY_REFERENCE:
                    if (!headers.has('content-range')) {
                        throw new Error(
                            'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?'
                        );
                    }
                    return {
                        data: json,
                        total: parseInt(
                            headers
                                .get('content-range')
                                .split('/')
                                .pop(),
                            10
                        ),
                    };
                case CREATE:
                    return { data: { ...params.data, id: json.id } };
                default:
                    return { data: json };
            }
        });
};

最后是 ASP.NET 控制器:

    [Route("api/[controller]")]
    [ApiController]
    public abstract class RaController<T> : ControllerBase, IRaController<T> where T : class, new()
    {

        protected readonly IDveDbContext _context;
        protected DbSet<T> _table;

        public RaController(IDveDbContext context)
        {
            _context = context;
            _table = _context.Set<T>();
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<T>> Get(int id)
        {
            var entity = await _table.FindAsync(id);

            if (entity == null)
            {
                return NotFound();
            }

            return entity;
        }

        [HttpPost]
        public async Task<ActionResult<T>> Post(T entity)
        {
            _table.Add(entity);
            await _context.SaveChangesAsync();
            var id = (int)typeof(T).GetProperty("Id").GetValue(entity);
            return Ok(await _table.FindAsync(id));
        }
    }

以及确切的 ProjectsController

[Route("api/[controller]")]
public class ProjectsController : RaController<Project>
{
    private IProjectService projectService;

    public ProjectsController(IProjectService projectService, IDveDbContext dbContext)
        : base (dbContext)
    {
        this.projectService = projectService;
    }
}

我一直在寻找解决方案,但找不到任何解决方案。如果有人提示问题可能出在哪里,或者提供一个成功地将 ASP.Net Core 与 React-Admin 集成的示例,我将非常感激!

标签: asp.netreactjsasp.net-core-webapireact-adminasp.net-core-3.1

解决方案


我已经让它与 .net REST API 很好地配合使用,从您开始使用的相同部分开始。我看到你的几个问题:

1. 使用更好的数据提供者。

内置提供程序更多的是一个示例。它仅适用于标识符“id”。 zachrybaker/ra-data-rest-client允许您对现有 API 的标识符名称具有更现实的灵活性。

2. 为您的基本控制器设计图案。

您的基本控制器需要一些调整。它应该描述它产生的内容(应用程序/json),并且 POST 方法是“创建”而不是“发布”,因为您需要区分创建(插入)和更新(因此是 404)。

我还要注意,如果需要,您的基本方法可以用来描述标识符的类型,而不是假设 int。这是我的。我希望将整个事情开源,敬请期待。

[ApiController]
[Produces("application/json")]
public abstract class EntityWithIdControllerBase<TEntity,  TReadModel, TCreateModel, TUpdateModel, TIndentifier> : BaseEFCoreAPIController
    where TEntity : class, IHaveIdentifier<TIndentifier>
{

////....
////constructor....
////....

    [HttpPost("")]

    public async virtual Task<ActionResult<TReadModel>> Create(CancellationToken cancellationToken, TCreateModel createModel)
    {
        return await CreateModel(createModel, cancellationToken);
    }
////....
    protected virtual async Task<TReadModel> CreateModel(TCreateModel createModel, CancellationToken cancellationToken = default(CancellationToken))
    {
        // create new entity from model
        var entity = Mapper.Map<TEntity>(createModel);

        // add to data set, id should be generated
        await DataContext
            .Set<TEntity>()
            .AddAsync(entity, cancellationToken);

        // save to database
        await DataContext
            .SaveChangesAsync(cancellationToken);

        // convert to read model
        var readModel = await ReadModel(entity.Id, cancellationToken);

        return readModel;
    }      
////....
    protected virtual async Task<TReadModel> ReadModel(TIndentifier id, CancellationToken cancellationToken = default(CancellationToken))
    {
        var model = await IncludeTheseForDetailResponse(DataContext
            .Set<TEntity>()
            .AsNoTracking())
            .Where(GetIdentifierEqualityFn(id))
            .ProjectTo<TReadModel>(Mapper.ConfigurationProvider)
            .FirstOrDefaultAsync(cancellationToken);

        return model;
    }
////....
    #region Expressions
    protected virtual Expression<Func<TEntity, bool>> IdentityExpressionFn(ExpressionStarter<TEntity> expressionStarter, JToken token)  
    {
        return expressionStarter.Or(GetIdentifierEqualityFn(token.Value<TIndentifier>()));
    }
    
    protected abstract Expression<Func<TEntity, bool>> GetIdentifierEqualityFn(TIndentifier o);
    
    /// <summary>
    /// When you need to include related entities on the detail response, you can add them via this IQueryable by overriding this function 
    /// </summary>
    /// <param name="queryable"></param>
    /// <returns></returns>
    protected virtual IQueryable<TEntity> IncludeTheseForDetailResponse(IQueryable<TEntity> queryable) => queryable;
    #endregion Expressions
}

只是让你知道,真正的乐趣从你开始处理 React admin 的过滤能力开始。这就是我的“表情”发挥作用的地方。我有一个基类,它可以利用表达式构建器并在真正需要时使用 LinqKit 进行动态处理。

我的许多控制器都是 Guid,所以我有一个中间控制器:

[ApiController]
[Produces("application/json")]
public abstract class EntityWithGuidIdController<TEntity,  TReadModel, TCreateModel, TUpdateModel> : 
    EntityWithIdControllerBase<TEntity,  TReadModel, TCreateModel, TUpdateModel, Guid>
    where TEntity : class, IHaveIdentifier<Guid>, new()
    
{
////....
////constructor....
////....
    #region Expressions
    /// <summary>
    /// If you need to use a primay key other than "Id", then override this.
    /// </summary>
    /// <param name="g"></param>
    /// <returns></returns>
    protected override Expression<Func<TEntity, bool>> GetIdentifierEqualityFn(Guid g)  => (x => x.Id == g);

    protected override Expression<Func<TEntity, bool>> IdentityExpressionFn(ExpressionStarter<TEntity> expressionStarter, JToken token)  
    {
        if (Guid.TryParse((string)token, out Guid g))
            return expressionStarter.Or(GetIdentifierEqualityFn(g));
    
        Debug.WriteLine((string)token + " Is NOT a valid Guid.  why was it sent?");
        return null;
    }
    #endregion Expressions
}

然后我可以像这样简单地使用它 -> 在这种情况下,详细响应还包括第二个对象:

[Route("api/[controller]")]
public class UserController : EntityWithGuidIdController<User, UserReadModel, UserCreateModel, UserUpdateModel>
{
    public UserController(ZachTestContext dataContext, IMapper mapper) : base(dataContext, mapper, CacheValidInterval.OneDay)
    { }

    // Include the Adloc on the detail response.
    protected override IQueryable<User> IncludeTheseForDetailResponse(IQueryable<User> queryable) => queryable.Include(x => x.Adloc);
}

推荐阅读