EF7多対多の関係。複数選択ドロップダウンからコントローラにデータを渡すには?

asp.net-core-mvc asp.net-mvc entity-framework entity-framework-core

質問

私のモデル

      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を使用してデータを正しく取得しており、ドロップダウンで複数の値を選択することができます。しかしコントローラーでは、乗客に値が渡されず、その数は0になります。ポストする前にドロップダウンの値をチェックし、選択した顧客のIDをコンマで表示します。私は乗客が整数配列ではないことを知っていますが、整数配列をモデルに追加すると別のエラーが発生するため、別の方法が必要であると考えていました。私は、私のビューモデルに文字列を追加し、この整数配列を文字列に追加する前に小さなハックを行いました。この文字列は、コントローラ内のすべての値(カンマsep)を持ちます。しかし、もっと良い方法があるはずだと確信しています。ビューからこの値を取得し、最終的にデータベースに格納するためのガイダンスは素晴らしいでしょう。

受け入れられた回答

私の現在のプロジェクトでは、多対多の関係がたくさんあります。私が知る限り、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>

次に、私は一般化されたUpdateクラス/メソッドを持っています。

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: http://stackoverflow.com/questions/5094489/how-do-i-dynamically-create-an-expressionfuncmyclass-bool-predicate-from-ex
        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;
    }
}

私はこのクラス/メソッドを呼び出すポストアクションで:

    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. ...");
    }


Related

ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ
ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ