我有一个带有EntityFramework Core的。net 5应用程序,我尝试从下拉列表中链接实体的类型:
我有一个商务类
public class Bar : IdEntity
{
public BarType BarType { get; set; }
public class BarType : IdEntity
{
public int Id { get; set; }
public string Name { get; set; }
为了不在视图中直接使用Bar
,我创建了一个DTO(数据传输对象,或者只是一个viewModel),如下所示:
public class BarDTO : IdEntityDTO
{
public int BarTypeId { get; set; }
public BarType BarType { get; set; }
In controller I do:
public class BarController : IdEntityController<Bar, BarDTO>
{
public override async Task<IActionResult> CreateAsync()
{
var barTypes=await _repository.ListAsync<BarType>();
ViewBag.BarTypes = barTypes;
return await base.CreateAsync();
}
在
@model MyApp.Web.ViewModels.BarDTO
<select asp-for="BarTypeId"
asp-items="@(new SelectList(ViewBag.BarTypes,
"Id", "Name"))">
<option value="">please select</option>
</select>
我的问题是如何将选择用户选择与Bar
的BarType
联系起来,以在具有相应类型id的数据库中创建有效的Bar
。
你的问题涉及所谓的model binding
在asp.net核心。更具体地说,您希望将int型Id
转换为BarType
的实例。从客户端发送的form
数据只是字符串值与相应的键配对。键就像指向相应模型属性的路径一样。因为服务器端的模型可能是具有深度属性的复杂类型图。所以key
可以是一个长点分隔的路径。在你的情况下,你的路径只是BarType
,它基本上针对错误的相应Id
。模型绑定器不能简单地将int
转换为BarType
的实例。正确路径为"BarType.Id
"。你可以写这样的代码:
<select asp-for="BarType.Id" asp-items="@(new SelectList(ViewBag.BarTypes, "Id", "Name"))">
<option value="">please select</option>
</select>
这将帮助模型绑定器使用接收到的Id
自动创建BarType
的实例。然而,我们只发送Id
,所以BarType
的实例只有Id
,所有Name
都是空的。Select
html元素只能保存一个映射到一个属性的值,通常是键值。在您的情况下,实际上模型根本不需要BarType.Name
。当我们处理可选择的数据时,我们只需要选择的键/id。记住这一点,我们就完成了上面的代码。
如果你还想收到BarType.Name
,我必须说这是错误的设计。除非从客户端发送的BarType.Name
可以编辑,但在这种情况下,显然它只是一个常数,就像BarType.Id
一样。在保存数据时,Id
是我们连接实体(建立关系)所需要的,其他属性根本不重要,实际上可以从Id
中派生/获取。
如果你仍然想要接收BarType.Name
,你至少有2个选择。第一个简单的方法是声明一个包含Id
和Name
的非映射属性,以某种方式构造计算值,以便可以从中提取每个单独的值。下面是一个例子:
public class BarType : IdEntity
{
public int Id { get; set; }
public string Name { get; set; }
//this property should be configured as not-mapped (ignored)
public string Id_Name {
get {
if(_id_Name == null){
_id_Name = $"{Id}_{Name}";
}
return _id_Name;
}
set {
_id_Name = value;
}
}
string _id_Name;
//a method to parse back the Id & Name
public BarType ParseIdName(){
if(!string.IsNullOrWhiteSpace(Id_Name)){
var parts = Id_Name.Split(new[] {'_'}, 2);
Id = int.TryParse(parts[0], out var id) ? id : 0;
Name = parts.Length > 1 ? parts[1] : null;
}
return this;
}
}
现在使用Id_Name
:
Id
作为所选值<select asp-for="BarType.Id_Name" asp-items="@(new SelectList(ViewBag.BarTypes, "Id_Name", "Name"))">
<option value="">please select</option>
</select>
注意,在控制器动作中实际使用模型中可用的绑定BarType
之前,您需要像这样手动调用BarType.ParseIdName
方法:
public override Task<IActionResult> Create(Bar entity)
{
entity.BarType?.ParseIdName();
//the entity is ready now ...
//...
return base.Create(entity);
}
第一个选项很简单,但有点棘手,我个人不会使用它。更标准的方法是使用第二个选项和自定义IModelBinder
。它以模型类型BarType
为目标,并将Id
解析为BarType
的实例。这个解析过程应该很快。
public class BarTypeModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var fieldValue = bindingContext.ValueProvider.GetValue(bindingContext.FieldName).FirstValue;
//here we just instantiate an instance of BarType with Id but without Name
if(int.TryParse(fieldValue, out var id)){
bindingContext.Result = ModelBindingResult.Succeed(new BarType { Id = id });
}
else {
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
你应该像这样使用模型绑定器:
[ModelBinder(typeof(BarTypeModelBinder))]
public class BarType : IdEntity
{
//...
}
差不多完成了。现在讨论如何从Id
中解析BarType
实例。正如我所说,通常我们只需要Id
,因此只需创建一个包含Id的BarType
实例(如上面的代码所做的)就足够了。这当然很快。如果您也需要Name
,您可能必须使用一些服务来解析BarType
的实例。因为需要速度快,所以确实需要某种in-memory
查找(或缓存数据)。假设您有一个服务从Id
中解析BarType
实例,如下所示:
public interface IBarTypeService {
BarType GetBarType(int id);
}
您可以在BarTypeModelBinder
中这样使用该服务:
if(int.TryParse(fieldValue, out var id)){
var barType = _barTypeService.GetBarType(id);
bindingContext.Result = ModelBindingResult.Succeed(barType);
}
else {
bindingContext.Result = ModelBindingResult.Failed();
}
对于初学者来说,第二个选项可能有点复杂(因为它涉及到一个支持缓存的漂亮的IBarTypeService
,或者至少是一个为它提供一些内存中数据查找的好方法),但实际上它是标准的方法。一旦你熟悉了这种逻辑,就会觉得很正常。