如何使用 LINQ to SQL 正确更新部分提交(到 REST API)的对象

本文关键字:REST API 对象 提交 to LINQ 何使用 SQL 更新部 | 更新日期: 2023-09-27 17:56:48

我们的 C# 应用程序通过 LINQ-to-SQL 数据库模型与数据库通信,特别是使用 MVC4 库。

我被分配了实现 RESTful API 的任务。我们认为对 API 进行版本控制是个好主意。这样,可以在新版本中引入对 API 的更改,并且现有 API 客户端不会中断。为了支持这一点,每个版本的 API 都有自己的一组公开和接受的数据传输对象 (DTO)。完成一些映射(使用自动映射器)以在 API 和数据库模型之间进行转换。

目前,我正在努力更新和创建功能。也就是说,如果客户端将Item对象 POST 到 ItemsController ,并且数据库中尚不存在该Item(给定其唯一标识符),则应创建一个新Item。如果数据库中已存在标识符,则应更新现有数据。目前为止,一切都好。

现在,我正在转换一个遗留代码库,以便与 RESTful API 通信,而不是直接与数据库通信。此代码库的某些部分更新资源上的单个属性,并仅发送该单个属性的标识符和新值。对象的其余部分应保持其在数据库中的状态。

我在使用 LINQ-to-SQL 实现这一点时遇到了麻烦,特别是因为 DTO 层。这是控制器方法:

[HttpPut]
[HttpPost]
public void UpdateOrCreateItem(ItemDTO data)
{
    Item submittedItem = Map(data);
    ItemRepository.UpdateOrCreateItem(submittedItem);
}

现在,不是接收完全填充的 data 对象,而是只填充标识符和另一个属性。当 LINQ-to-SQL 按如下所示处理此数据时:

public static void UpdateOrCreateItem(Item submittedItem)
{
    if (submittedItem.Id > 0)
    {
        // update
        using (DatabaseAccessor db = new DatabaseAccessor())
        {
            db.context.Items.Attach(submittedItem);
            db.context.Refresh(RefreshMode.KeepCurrentValues, submittedItem);
            db.context.SubmitChanges();
        }
    } else {
        // create
        // omitted...
    }
}

刷新将所有空(缺失)属性标记为已更改,并将其全部保存到数据库中。相反,只应存储 REST API 级别提交的属性。这个问题的优雅解决方案是什么?

如何使用 LINQ to SQL 正确更新部分提交(到 REST API)的对象

最后,

我编写了一些代码来接受json补丁请求(请参阅 https://www.rfc-editor.org/rfc/rfc6902)。

  • 您需要将媒体类型"application/json-patch"添加到接受格式的集合中。
  • 您需要接受标识符和 JsonPatchOperation 对象数组作为 API 控制器上 HTTP PATCH 方法的输入

API 控制器方法:

[HttpPatch]
public void UpdatePartially(int id, JsonPatchOperation[] patchOperations)
{
    if (id > 0)
    {
        // DatabaseAccessor is just a wrapper around my DataContext object
        using (DatabaseAccessor db = new DatabaseAccessor())
        {
            SetDataLoadOptions(db); // optional of course
            var item = db.context.Items.Single(i => i.id == id);
            foreach (JsonPatchOperation patchOperation in patchOperations)
            {
                // when you want to set a foreign key identifier, LINQ-to-SQL throw a ForeignKeyReferenceAlreadyHasValueException
                // the patchOperation will then use GetForeignKeyObject to fetch the object that it requires to set the foreign key object instead
                patchOperation.GetForeignKeyObject = (PropertyInfo property, object identifier) =>
                {
                    // this is just example code, make sure to correct this for the possible properties of your object...
                    if (property == typeof(Item).GetProperty("JobStatus", typeof(JobStatus)))
                    {
                        return db.context.JobStatus.Single(js => js.StatusId == (int)identifier);
                    }
                    else if (property == typeof(Item).GetProperty("User", typeof(User)))
                    {
                        return db.context.Users.Single(u => u.UserId == (Guid)identifier);
                    }
                    throw new ArgumentOutOfRangeException("property", String.Format("Missing getter for property '{0}'.", property.Name));
                };
                patchOperation.ApplyTo(item);
            }
            db.context.SubmitChanges();
        }
    }
}

以下是上述方法的依赖项:

/// <summary>
/// Add this to the global configuration Formatters collection to accept json-patch requests
/// </summary>
public class JsonPatchMediaTypeFormatter : JsonMediaTypeFormatter
{
    public JsonPatchMediaTypeFormatter() : base()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json-patch"));
    }
}
/// <summary>
/// All possible values for the "op" property of a json-patch object
/// docs: https://www.rfc-editor.org/rfc/rfc6902#section-4
/// </summary>
public enum JsonPatchOperationType
{
    add,
    remove,
    replace,
    move,
    copy,
    test
}
/// <summary>
/// json-patch is a partial update format for HTTP PATCH requests
/// docs: https://www.rfc-editor.org/rfc/rfc6902
/// </summary>
public class JsonPatchOperation
{
    public string op { get; set; }
    public string from { get; set; }
    public string path { get; set; }
    public string value { get; set; }
    public Func<PropertyInfo, object, object> GetForeignKeyObject { get; set; }
    public JsonPatchOperationType Operation
    {
        get
        {
            return (JsonPatchOperationType)Enum.Parse(typeof(JsonPatchOperationType), op);
        }
    }
    public void ApplyTo(object document)
    {
        switch (Operation)
        {
            case JsonPatchOperationType.add:
                Add(document, path, value);
                break;
            case JsonPatchOperationType.remove:
                Remove(document, path);
                break;
            case JsonPatchOperationType.replace:
                Replace(document, path, value);
                break;
            case JsonPatchOperationType.move:
                Move(document, path, from);
                break;
            case JsonPatchOperationType.copy:
                Copy(document, path, from);
                break;
            case JsonPatchOperationType.test:
                Test(document, path, value);
                break;
        }
    }
    private void Add(object document, string path, string value)
    {
        Type documentType = document.GetType();
        PathInfo pathInfo = GetPathInfo(documentType, path);
        object convertedValue = ConvertToType(value, pathInfo.PropertyInfo.PropertyType);
        pathInfo.PropertyInfo.SetValue(document, convertedValue, pathInfo.Indexes);
    }
    private void Replace(object document, string path, string value)
    {
        Type documentType = document.GetType();
        PathInfo pathInfo = GetPathInfo(documentType, path);
        object convertedValue = ConvertToType(value, pathInfo.PropertyInfo.PropertyType);
        try
        {
            pathInfo.PropertyInfo.SetValue(document, convertedValue, pathInfo.Indexes);
        }
        // gnarly hack for setting foreign key properties
        catch (TargetInvocationException tie)
        {
            if (tie.InnerException is ForeignKeyReferenceAlreadyHasValueException)
            {
                PropertyInfo matchingProperty = documentType.GetProperties().Single(p => p.GetCustomAttributes(typeof(AssociationAttribute), true).Any(attr => ((AssociationAttribute)attr).ThisKey == pathInfo.PropertyInfo.Name));
                matchingProperty.SetValue(document, GetForeignKeyObject(matchingProperty, convertedValue), null);
            }
            else
            {
                throw tie;
            }
        }
    }
    private void Remove(object document, string path)
    {
        Type documentType = document.GetType();
        PathInfo pathInfo = GetPathInfo(documentType, path);
        pathInfo.PropertyInfo.SetValue(document, GetDefaultValue(pathInfo.PropertyInfo.PropertyType), pathInfo.Indexes);
    }
    private void Copy(object document, string path, string from)
    {
        throw new NotImplementedException();
    }
    private void Move(object document, string path, string from)
    {
        throw new NotImplementedException();
    }
    private void Test(object document, string path, string value)
    {
        throw new NotImplementedException();
    }
    #region Util
    private class PathInfo
    {
        public PropertyInfo PropertyInfo { get; set; }
        public object[] Indexes { get; set; }
    }
    private PathInfo GetPathInfo(Type documentType, string path)
    {
        object[] indexes = null;
        PropertyInfo propertyInfo = documentType.GetProperty(path);
        return new PathInfo { PropertyInfo = propertyInfo, Indexes = indexes };
    }
    private object GetDefaultValue(Type t)
    {
        if (t.IsValueType)
            return Activator.CreateInstance(t);
        return null;
    }
    private object ConvertToType(string value, Type type)
    {
        TypeConverter typeConverter = TypeDescriptor.GetConverter(type);
        return typeConverter.ConvertFromString(value);
    }
    #endregion
}

很明显,这还没有完成,成熟或优雅。但它有效。