通过 Web Api 发送项目列表的更好方法

Better way to send a list of items trough WebApi

我有一个 Web API 配置为发送一个 POST 到 SQL 服务器。 我还有一个服务器应用程序 (SignalR),它向 POST API.

发送一个字符串列表

问题:Post每次只接收一个项目,所以我在循环中多次发送请求,每次迭代都会发送一个新项目。

它有效,但我相信有一种优化的方法可以做到这一点,如果在迭代中出现问题,正确的做法是取消交易,但使用这种循环方法,这是不可能的。 我正在接受有关如何更好地处理此问题的提示。

WebApi:

VisitaItemControl.cs

public class VisitaItemControl
{
    public string ItemID { get; set; }
    public string VisitaID { get; set; }
}

VisitaItemControlController.cs

[Route("api/[controller]")]
    [ApiController]
    public class VisitaItemControlController : ControllerBase
    {
        private readonly IConfiguration _configuration;
        public VisitaItemControlController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

    [HttpPost]
    public JsonResult Post(VisitaItemControl visitaItemControl)
    {
        string query = @"INSERT INTO VisitaItemControl (
                        ItemID,
                        VisitaID)
                        VALUES (
                        @ItemID,
                        @VisitaID
                        )";
        DataTable dt = new DataTable();
        string sqlDataSource = _configuration.GetConnectionString("connectionstring");
        SqlDataReader sqlDataReader;
        using (SqlConnection sqlConnection = new SqlConnection(sqlDataSource))
        {
            sqlConnection.Open();

            using (SqlCommand cmd = new SqlCommand(query, sqlConnection))
            {
                cmd.Parameters.AddWithValue(@"ItemID", visitaItemControl.ItemID);
                cmd.Parameters.AddWithValue(@"VisitaID", visitaItemControl.VisitaID);
                sqlDataReader = cmd.ExecuteReader();
                dt.Load(sqlDataReader);
                sqlDataReader.Close();
                sqlConnection.Close();
            }
        }

        return new JsonResult("Saved!");
    }
}

SignalR 应用程序:

    foreach (var item in addedItems)
    {
        var postObject = new VisitaItemControl() 
        {
        ItemID = item.ItemID,
        VisitaID = empObj.VisitaID,
        };
        var request2 = new HttpRequestMessage(HttpMethod.Post, config["API_POST"]);
        request2.Content = new StringContent(JsonSerializer.Serialize(postObject), null, "application/json");
        var client2 = ClientFactory.CreateClient();
        var response2 = await client.SendAsync(request2);
        using var responseStream2 = await response2.Content.ReadAsStreamAsync();
        string res2 = await JsonSerializer.DeserializeAsync<string>(responseStream2);
    }

    await JS.InvokeVoidAsync("alert", "Saved!");
    await refreshList();
    uriHelper.NavigateTo("/", forceLoad: true);
}

以下是您尝试做的事情的结构化方法的基础知识。

我已经使用 Entity Framework 来管理数据库和 InMemory Implemnentation 用于演示目的。我已经在 Blazor Server 项目中实现了所有内容,因此我们可以测试和管理 UI 中的数据,并使用 Postman 与 API.

进行交互

项目包:

<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.4" />
    <PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup>

我的 InMemory 数据库上下文:

public class InMemoryDbContext : DbContext
{
    public DbSet<VisitaItemControl>? VisitaItemControl { get; set; }
    public InMemoryDbContext(DbContextOptions<InMemoryDbContext> options) : base(options) { }
}

我们将使用 DBContextFactory 服务来管理将使用它作为源 DbContext 的数据库连接。

我的数据代理接口 - 这通常会实现所有 CRUD 进程。我们使用接口将应用程序与数据存储分离。

public interface IDataBroker
{
    public ValueTask<bool> AddItems<TRecord>(IEnumerable<TRecord> items) where TRecord : class;
    public ValueTask<IEnumerable<TRecord>> GetItems<TRecord>(int count) where TRecord : class;
}

还有我的服务器实现 - 注意我注入了 DbContextFactory 来管理我的数据库连接。

public class ServerDataBroker : IDataBroker
{
    private readonly IDbContextFactory<InMemoryDbContext> database;
    
    public ServerDataBroker(IDbContextFactory<InMemoryDbContext> db)
        => this.database = db;

    public async ValueTask<bool> AddItems<TRecord>(IEnumerable<TRecord> items) where TRecord : class
    {
        var result = false;

        using var dbContext = database.CreateDbContext();
        foreach (var item in items)
            dbContext.Add(item);

        var rowsAdded = await dbContext.SaveChangesAsync();
        if (rowsAdded == items.Count())
            result = true;

        //  Do something if not all rows are added
        return result;
    }

    public async ValueTask<IEnumerable<TRecord>> GetItems<TRecord>(int count) where TRecord : class
    {
        using var dbContext = database.CreateDbContext();
        return await dbContext.Set<TRecord>()
            .Take(count)
            .ToListAsync();
    }
}

对于 UI,我构建了一个非常简单的视图服务来保存和管理数据:

public class VisitaItemControlService
{
    private IDataBroker _broker;

    public event EventHandler? ListUpdated;
    public IEnumerable<VisitaItemControl> Records { get; protected set; } = new List<VisitaItemControl>();

    public VisitaItemControlService(IDataBroker dataBroker)
        => _broker = dataBroker;

    public async ValueTask<bool> AddItems(IEnumerable<VisitaItemControl> items)
    {
        var result = await _broker.AddItems<VisitaItemControl>(items);

        if (result)
        {
            await this.GetItems(1000);
            this.ListUpdated?.Invoke(this, EventArgs.Empty);
        }

        return result;
    }

    public async ValueTask GetItems(int count)
        => this.Records = await _broker.GetItems<VisitaItemControl>(count);
}

这是我用来测试系统的索引页面。

@page "/"
@inject VisitaItemControlService service;
<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>
<div>
    <button class="btn btn-primary" @onclick=AddItems>Add Some Items</button>
</div>

@if (loaded)
{
    @foreach (var item in this.service.Records)
    {
        <div  class="p-2">
            <span>
                Item : @item.ItemID
            </span>
            <span>
                Visita : @item.VisitaID
            </span>
        </div>
    }
}

@code {
    private bool loaded = false;

    protected async override Task OnInitializedAsync()
    {
        await this.service.GetItems(1000);
        this.service.ListUpdated += this.OnListUpdated;
        this.loaded = true;
    }

    private async Task AddItems()
    {
        var addList = new List<VisitaItemControl> {
        new VisitaItemControl { ItemID = Guid.NewGuid().ToString(), VisitaID = "AA" },
        new VisitaItemControl { ItemID = Guid.NewGuid().ToString(), VisitaID = "BB" },
        new VisitaItemControl { ItemID = Guid.NewGuid().ToString(), VisitaID = "CC" }
        };

        await this.service.AddItems(addList);
    }

    private void OnListUpdated(object? sender, EventArgs e)
        => this.InvokeAsync(StateHasChanged);
}

注意使用事件通知 UI 列表已更改并触发 re-render。

这是我的 API 控制器:

[ApiController]
public class VisitaItemControlController : ControllerBase
{
    private IDataBroker _dataBroker;

    public VisitaItemControlController(IDataBroker dataBroker)
        => _dataBroker = dataBroker;

    [Route("/api/[controller]/list")]
    [HttpGet]
    public async Task<IActionResult> GetRecordsAsync()
    {
        var list = await _dataBroker.GetItems<VisitaItemControl>(1000);
        return Ok(list);
    }

    [Route("/api/[controller]/addlist")]
    [HttpPost]
    public async Task<bool> AddRecordsAsync([FromBody] IEnumerable<VisitaItemControl> records)
        => await _dataBroker.AddItems(records);
}

最后Program配置所有服务和中间件。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddDbContextFactory<InMemoryDbContext>(options => options.UseInMemoryDatabase("TestDb"));
builder.Services.AddSingleton<IDataBroker, ServerDataBroker>();
builder.Services.AddScoped<VisitaItemControlService>();

builder.Services.AddSingleton<WeatherForecastService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

一些 postman 屏幕截图:

以及项目结构: