如何使用 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 级别提交的属性。这个问题的优雅解决方案是什么?
我编写了一些代码来接受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
}
很明显,这还没有完成,成熟或优雅。但它有效。