EF7 多对多关系。如何将数据从 multi select 下拉列表传递到控制器?

EF7 many to many relationship. How to pass data from multi select dropdown to controller?

我的模型

      public class FlightBooking
        {
            public int Id { get; set; }                              
            public ICollection<FlightPassenger> Passengers { get; set; }            
            public DateTime DateJourney { get; set; }
            public virtual City FromCity { get; set; }
            public virtual City ToCity { get; set; }
        }

     public class FlightPassenger
       {
            public int FlightBookingId { get; set; }
            public FlightBooking FlightBooking { get; set; }

            public int CustomerId { get; set; }
            public Customer Passenger { get; set; }
       }

     public class Customer
     {
        public int Id { get; set; }       
        public string FirstName { get; set; }
        public string LastName { get; set; }       
        public string Gender { get; set; }
        public DateTime BirthDate { get; set; }        
        public ICollection<FlightPassenger> FlightPassengers { get; set; }

     }

我在 OnModelCreating 中添加了

modelBuilder.Entity<FlightPassenger>().HasKey(x => new { x.FlightBookingId, x.CustomerId });

这将在数据库中创建 3 个表。客户、FlightBooking 和 FlightPassenger。所有这些都可以很好地表示 EF7 中的多对多关系。现在我正在尝试从用户那里获取此输入。

我的观点

<select asp-for="Passengers" asp-items="Enumerable.Empty<SelectListItem>()" class="form-control customer"></select>

我正在使用 Ajax 正确获取数据,并且能够 select 下拉列表中的多个值。但是在控制器中,Passengers 中没有传递任何值,其计数为 0。我在发布前检查了下拉列表中的值,它显示了 selected 客户的 ids,并带有逗号。我知道 Passengers 不是一个整数数组,但是向模型中添加一个整数数组会产生另一个错误,所以我认为必须有另一种方法。我通过向我的视图模型添加一个字符串并在发布之前将这个整数数组添加到字符串来做了一个小改动。该字符串包含控制器中的所有值(逗号分隔符)。但我相信应该有更好的方法。任何关于从视图中获取此值并最终存储在数据库中的指导都会很棒。

在我目前的项目中,我有很多多对多的关系。据我所知,EF Core 尚不支持多对多,因此我认为它必须手动完成。我概括了解决方案。

由于我是 EF/MVC 的新手,欢迎反馈:

首先,我创建了一个 JoinContainer 来保存多对多实体的必要数据。

public class SimpleJoinContainerViewModel
{
    public int[] SelectedIds { get; set; }
    public IEnumerable<SelectListItem> SelectListItems { get; set; }

    // keeping track of the previously selected items
    public string PreviousSelectedHidden { get; set; }
    public int[] PreviousSelectedIds
    {
        get
        {
            // if somebody plays around with the hidden field containing the ints the standard exception/error page is ok:
            return PreviousSelectedHidden?.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(int.Parse).ToArray();
        }
        private set { PreviousSelectedHidden = value == null ? "" : string.Join(" ", value); }
    }

    /// <summary>
    /// Call when form is loaded - not on post back
    /// </summary>
    /// <param name="selectListItems"></param>
    /// <param name="selectedIds">Currently selected referenced ids. Get via m:n/join-table</param>
    public void Load(IEnumerable<SelectListItem> selectListItems, IEnumerable<int> selectedIds)
    {
        SelectListItems = selectListItems;
        SelectedIds = selectedIds?.ToArray();
        PreviousSelectedIds =  SelectedIds;
    }
}

在(FlightBooking)的视图模型中:

[Display(Name = "Passengers")]
public SimpleJoinContainerViewModel PassengersJoinContainer { get; set; } = new SimpleJoinContainerViewModel();

在 GET 操作中,我使用 Load() 方法用数据填充容器:

viewModel.PassengerJoinContainer.Load(
    DbContext.Customers
        .Select(s => new SelectListItem
        {
            Text = s.LastName,
            Value = s.Id.ToString()
        }),
    flightBookingEntity?.Passengers?.Select(p => p.CustomerId));

在视图中我使用了 JoinContainer 的属性:

<div class="form-group">
    <label asp-for="PassengersJoinContainer" class="col-sm-3 control-label"></label>
    <div class="col-sm-9">
        <div class="nx-selectize">
            @Html.ListBoxFor(m => m.PassengersJoinContainer.SelectedIds, Model.PassengersJoinContainer.SelectListItems)
        </div>
        @Html.HiddenFor(m => m.PassengersJoinContainer.PreviousSelectedHidden)
        <span asp-validation-for="PassengersJoinContainer" class="text-danger"></span>
    </div>
</div>

然后我有一个通用的更新class/method。

public class SimpleJoinUpdater<T> where T : class, new()
{
    private DbContext DbContext { get; set; }
    private DbSet<T> JoinDbSet { get; set; }
    private Expression<Func<T, int>> ThisJoinIdColumn { get; set; }
    private Expression<Func<T, int>> OtherJoinIdColumn { get; set; }
    private int ThisEntityId { get; set; }
    private SimpleJoinContainerViewModel SimpleJoinContainer { get; set; }

    /// <summary>
    /// Used to update many-to-many join tables.
    /// It uses a hidden field which holds the space separated ids
    /// which existed when the form was loaded. They are compared
    /// to the current join-entries in the database. If there are 
    /// differences, the method returns false.
    /// Then it deletes or adds join-entries as needed.
    /// Warning: this is not completely safe. A race condition
    /// may occur when the update method is called concurrently
    /// for the same entities. (e.g. 2 persons press the submit button at the same time.)
    /// </summary>
    /// <typeparam name="T">Type of the many-to-many/join entity</typeparam>
    /// <param name="dbContext">DbContext</param>
    /// <param name="joinDbSet">EF-context dbset for the join entity</param>
    /// <param name="thisJoinIdColumn">Expression to the foreign key (Id/int) which points to the current entity</param>
    /// <param name="otherJoinIdColumn">Expression to the foreign key (Id/int) which points to the joined entity</param>
    /// <param name="thisEntityId">Id of the current entity</param>
    /// <param name="simpleJoinContainer">Holds selected ids after form post and the previous selected ids</param>
    /// <returns>True if updated. False if data has been changed in the database since the form was loaded.</returns>
    public SimpleJoinUpdater(
        DbContext dbContext,
        DbSet<T> joinDbSet,
        Expression<Func<T, int>> thisJoinIdColumn,
        Expression<Func<T, int>> otherJoinIdColumn,
        int thisEntityId,
        SimpleJoinContainerViewModel simpleJoinContainer
    )
    {
        DbContext = dbContext;
        JoinDbSet = joinDbSet;
        ThisJoinIdColumn = thisJoinIdColumn;
        OtherJoinIdColumn = otherJoinIdColumn;
        ThisEntityId = thisEntityId;
        SimpleJoinContainer = simpleJoinContainer;
    }


    public bool Update()
    {
        var previousSelectedIds = SimpleJoinContainer.PreviousSelectedIds;

        // load current ids of m:n joined entities from db:
        // create new boolean expression out of member-expression for Where()
        // see: 
        ParameterExpression parameterExpression = Expression.Parameter(typeof (T), "j");
        var propertyName = ((MemberExpression) ThisJoinIdColumn.Body).Member.Name;
        Expression propertyExpression = Expression.Property(parameterExpression, propertyName);
        var value = Expression.Constant(ThisEntityId);
        Expression equalExpression = Expression.Equal(propertyExpression, value);
        Expression<Func<T, bool>> thisJoinIdBooleanExpression =
            Expression.Lambda<Func<T, bool>>(equalExpression, parameterExpression);

        var joinedDbIds = JoinDbSet
            .Where(thisJoinIdBooleanExpression)
            .Select(OtherJoinIdColumn).ToArray();


        // check if ids previously (GET) and currently (POST) loaded from the db are still the same
        if (previousSelectedIds == null)
        {
            if (joinedDbIds.Length > 0) return false;
        }
        else
        {
            if (joinedDbIds.Length != previousSelectedIds.Length) return false;
            if (joinedDbIds.Except(previousSelectedIds).Any()) return false;
            if (previousSelectedIds.Except(joinedDbIds).Any()) return false;
        }


        // create properties to use as setters:
        var thisJoinIdProperty = (PropertyInfo) ((MemberExpression) ThisJoinIdColumn.Body).Member;
        var otherJoinIdProperty = (PropertyInfo) ((MemberExpression) OtherJoinIdColumn.Body).Member;

        // remove:
        if (joinedDbIds.Length > 0)
        {
            DbContext.RemoveRange(joinedDbIds.Except(SimpleJoinContainer.SelectedIds).Select(id =>
            {
                var e = new T();
                thisJoinIdProperty.SetValue(e, ThisEntityId);
                otherJoinIdProperty.SetValue(e, id);
                return e;
            }));
        }

        // add:
        if (SimpleJoinContainer.SelectedIds?.Length > 0)
        {
            var toAddIds = SimpleJoinContainer.SelectedIds.Except(joinedDbIds).ToList();
            if (toAddIds.Count > 0)
            {
                DbContext.AddRange(SimpleJoinContainer.SelectedIds.Except(joinedDbIds).Select(id =>
                {
                    var e = new T();
                    thisJoinIdProperty.SetValue(e, ThisEntityId);
                    otherJoinIdProperty.SetValue(e, id);
                    return e;
                }));
            }
        }
        return true;
    }
}

在 Post 动作中,我称其为 class/method:

    var flightPassengersUpdater = new SimpleJoinUpdater<FlightPassenger>(
            DbContext,
            DbContext.FlightPassengers,
            mm => mm.FlightBookingId,
            mm => mm.CustomerId,
            model.Id,  // model = current flightBooking object
            viewModel.PassengersJoinContainer);
    if (!flightPassengersUpdater .Update())
    {
        ModelState.AddModelError("PassengersJoinContainer", "Since you opened this form the data has already been altered by someone else. ...");
    }