2023年1月

方式一

// 使用计算器来设置每次同时运行的任务数量
var semaphore = new SemaphoreSlim(TaskNumber);
var tasks = new Task[projects.Count];
for (int i = 0; i < projects.Count; i++)
{
    var project = projects[i];

    // 在每个任务之前,先等待信号量授予
    await semaphore.WaitAsync();

    tasks[i] = new Task(async () =>
    {
        try
        {
            _logger.LogInformation($"开始执行:第{i + 1}个,总共:{projects.Count}个,当前ProjectId为:{project.Id}");
            await ReSaveAllProjectTask(project, i, projects.Count);
        }
        catch (Exception e)
        {
            errorProjectIds.Add(project.Id);
        }
        finally
        {
            // 无论任务是否成功完成,都要释放信号量
            semaphore.Release();
        }
    });
    tasks[i].Start();
}
// 等待所有任务完成
Task.WaitAll(tasks);

方式二

ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = TaskNumber };
await Parallel.ForEachAsync(projects, parallelOptions, async (project, cancel) =>
{
    try
    {
        var i = projects.IndexOf(project);
        _logger.LogInformation($"开始执行:第{i + 1}个,总共:{projects.Count}个,当前ProjectId为:{project.Id}");
        await ReSaveAllProjectTask(project, i, projects.Count);
    }
    catch (Exception)
    {
        errorProjectIds.Add(project.Id);
    }
});

后端代码:

using System.Diagnostics.CodeAnalysis;
using TB.ComponentModel;// 需要安装依赖 UniversalTypeConverter

[HttpPost]
public async Task<List<DFileInfoDto>> BatchUploadFileAsync(IFormCollection Form)
{
    var tokenDTO = await TokenHelper.GetCurrentTokenAsync(HttpContext);

    if (tokenDTO == null)
    {
        throw new CustomException(ErrCodes.DocTokenGetDataFail, "从Token中获取用户信息失败");
    }

    // Request.Form or Form
    var files = Form.Files;
    var list2 = IFormCollectionToGeneric<DFileInfoDto>(Form);

    var res = new List<DFileInfoDto>();
    for (int i = 0; i < files.Count; i++)
    {
        list2[i].FromFile = files[i];

        var fileInfoDto = await _document.UploadFileAsync(list2[i], tokenDTO);
        res.Add(fileInfoDto);
    }

    return res;
}


public static List<T> IFormCollectionToGeneric<T>(IFormCollection formCollection) where T : class, new()
{
    var properties = typeof(T).GetProperties();
    var propertiesNames = properties.Select(p => p.Name).ToList();
    var ignoreCaseComparer = new IgnoreCaseComparer();
    var keys = formCollection.Keys.Where(key => propertiesNames.Contains(key, ignoreCaseComparer)).ToList();
    var formCount = formCollection[keys.First()].Count;

    var list = new List<T>();
    for (int i = 0; i < formCount; i++)
    {
        var instance = Activator.CreateInstance<T>();
        foreach (var property in properties)
        {
            if (formCollection.TryGetValue(property.Name, out var stringValues))
            {
                var value = property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null;
                if (stringValues[i].IsConvertibleTo(property.PropertyType))
                {
                    value = stringValues[i].To(property.PropertyType);
                }
                property.SetValue(instance, value);
            }
        }
        list.Add(instance);
    }
    return list;
}

public class IgnoreCaseComparer : IEqualityComparer<string>
{
    public bool Equals(string? x, string? y)
    {
        return x.Equals(y, StringComparison.OrdinalIgnoreCase);
    }

    public int GetHashCode([DisallowNull] string obj)
    {
        return obj.ToUpper().GetHashCode();
    }
}

前端代码:
前端代码不完整,仅供参考,其原理为:将需要请求的对象数组,全部转换为FormData对象,因FormData对象没有对象数组的概念,所以FormData对象中的key是可重复的,在后端同个key可以接收到多个value

  const form = new FormData();
  dto.data!.forEach((t) => {
    const keys = Object.keys(t);
    keys.forEach((k) => {
      form.append(k, t[k]);
    });
  });
  dto.data = form;
  
  
  
  function PostApi<T = any>(url, data, headers?: any): Promise<ApiResultDTO<T>> {
  url = FormatUrl(url);
  let config: ConfigType = {
    headers: {},
    method: "POST",
    body: data != null ? JSON.stringify(data) : null,
  };
  if (data) {
    if (data instanceof FormData) {
      config.body = data;
    } else if (headers && headers["Content-Type"] === "multipart/form-data") {
      config.body = data;
    } else {
      config.body = JSON.stringify(data);
      config.headers["Content-Type"] = "application/json";
    }
  } else {
    config.body = null;
  }

  if (headers) {
    Object.assign(config.headers, headers);
  }
  return new Promise((resolve, reject) => {
    setLoading(true);
    fetch(url, config).then((res) => handleJsonResult(resolve, reject, res));
  });
}