你注意到 .Net Framework 和 .Net Core 中使用 Session 的區別了嗎?

原由

在測試一個例子時發現的問題,這個示例實現的功能是刷新頁面也能保持表格鎖定列的狀態,先看下頁面的完成效果:數據庫

 

測試中發現,幾乎相同的代碼:編程

 

這個例子使用了 Session 來保存表格的鎖定狀態,先來看下頁面視圖的定義:數組

@(F.Grid().IsFluid(true).CssClass("blockpanel").Title("表格").ShowHeader(true).ShowBorder(true).ID("Grid1").DataIDField("Id").DataTextField("Name").AllowColumnLocking(true)
    .Columns(
        F.RowNumberField(),
        F.RenderField().HeaderText("姓名").DataField("Name").Width(100).EnableLock(true).Locked(true),
        F.RenderField().HeaderText("性別").DataField("Gender").FieldType(FieldType.Int).RendererFunction("renderGender").Width(80).EnableLock(true),
        F.RenderField().HeaderText("入學年份").DataField("EntranceYear").FieldType(FieldType.Int).Width(100).EnableLock(true),
        F.RenderCheckField().HeaderText("是否在校").DataField("AtSchool").RenderAsStaticField(true).Width(100).EnableLock(true),
        F.RenderField().HeaderText("所學專業").DataField("Major").RendererFunction("renderMajor").Width(300).EnableLock(true),
        F.RenderField().HeaderText("分組").DataField("Group").RendererFunction("renderGroup").Width(80).EnableLock(true),
        F.RenderField().HeaderText("註冊日期").DataField("LogTime").FieldType(FieldType.Date).Renderer(Renderer.Date).RendererArgument("yyyy-MM-dd").Width(100).EnableLock(true)
    ).Listener("columnlock", "onGridColumnLock").Listener("columnunlock", "onGridColumnUnlock")
    .DataSource(DataSourceUtil.GetDataTable())
)

 

在客戶端事件 columnlock 和 columnunlock 中,會將鎖定列的狀態改變回發到後臺:服務器

function onGridColumnLock(event, columnId) {
    // 觸發後臺事件
    F.doPostBack('@Url.Action("Grid1_ColumnLockUnlock")', {
        type: 'lock',
        columnId: columnId
    });
}

function onGridColumnUnlock(event, columnId) {
    // 觸發後臺事件
    F.doPostBack('@Url.Action("Grid1_ColumnLockUnlock")', {
        type: 'unlock',
        columnId: columnId
    });
}

 

後臺會將列狀態信息保存到 Session 中(實際項目中是要保存到數據庫中的):session

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Grid1_ColumnLockUnlock(string type, string columnId)
{
    // 模擬操做數據庫中的數據
    List<string> lockedColumns = GetLockedColumns();
    if (type == "lock")
    {
        if (!lockedColumns.Contains(columnId))
        {
            lockedColumns.Add(columnId);
        }
    }
    else if (type == "unlock")
    {
        if (lockedColumns.Contains(columnId))
        {
            lockedColumns.Remove(columnId);
        }
    }

    return UIHelper.Result();
}


private static readonly string KEY_FOR_DATASOURCE_SESSION = "GridLockColumn.SaveToDB";

// 模擬在服務器端保存數據
// 特別注意:在真實的開發環境中,不要在Session放置大量數據,不然會嚴重影響服務器性能
private List<string> GetLockedColumns()
{
    if (Session[KEY_FOR_DATASOURCE_SESSION] == null)
    {
        Session[KEY_FOR_DATASOURCE_SESSION] = new List<string>() { };
    }
    return (List<string>)Session[KEY_FOR_DATASOURCE_SESSION];
}

固然,上面對 Session 的操做是在 FineUIMvc(ASP.NET MVC) 中的代碼,也就是運行在 .Net  Framework 下的代碼。mvc

 

FineUICore(ASP.NET Core)中的代碼稍微不一樣,以下所示:app

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Grid1_ColumnLockUnlock(string type, string columnId)
{
    // 模擬操做數據庫中的數據
    List<string> lockedColumns = GetLockedColumns();
    if (type == "lock")
    {
        if (!lockedColumns.Contains(columnId))
        {
            lockedColumns.Add(columnId);
        }
    }
    else if (type == "unlock")
    {
        if (lockedColumns.Contains(columnId))
        {
            lockedColumns.Remove(columnId);
        }
    }

    return UIHelper.Result();
}


private static readonly string KEY_FOR_DATASOURCE_SESSION = "GridLockColumn.SaveToDB";

// 模擬在服務器端保存數據
// 特別注意:在真實的開發環境中,不要在Session放置大量數據,不然會嚴重影響服務器性能
private List<string> GetLockedColumns()
{
    if (HttpContext.Session.GetObject<List<string>>(KEY_FOR_DATASOURCE_SESSION) == null)
    {
        HttpContext.Session.SetObject(KEY_FOR_DATASOURCE_SESSION, new List<string>() { });
    }
    return HttpContext.Session.GetObject<List<string>>(KEY_FOR_DATASOURCE_SESSION);
}

 

上面是保存狀態的邏輯,而刷新頁面後,會從Session中讀取保存的列鎖定狀態:ide

// GET: GridLockColumn/SaveToDB
public ActionResult Index()
{
    LoadData();

    return View();
}

private void LoadData()
{
    ViewBag.LockedColumns = GetLockedColumns();
}

而後,在頁面視圖中,將保存的列鎖定狀態設置到表格上,以下所示:性能

@{
    Grid grid1 = F.GetControl<Grid>("Grid1");

    List<string> lockedColumns = ViewBag.LockedColumns as List<string>;
    if (lockedColumns.Count > 0)
    {
        foreach (GridColumn column in grid1.Columns)
        {
            RenderBaseField field = column as RenderBaseField;
            if (field == null)
            {
                continue;
            }

            if (lockedColumns.Contains(field.ColumnID) || lockedColumns.Contains(field.DataField))
            {
                field.Locked = true;
            }

        }
    }
}

 

至此,整個流程所有完成。問題是,幾乎如出一轍的代碼,爲何在 .Net Framework 下一切正常,而 .Net Core 下卻出問題了?測試

 

溯源

通過代碼調試,咱們發現,在 .Net Core 下將狀態保存到 Session 中後,再去 Session 中檢查卻不存在!

後來才發現,咱們過於相信引用類型了,請看以下代碼:

// 模擬操做數據庫中的數據
List<string> lockedColumns = GetLockedColumns();
if (type == "lock")
{
    if (!lockedColumns.Contains(columnId))
    {
        lockedColumns.Add(columnId);
    }
}
else if (type == "unlock")
{
    if (lockedColumns.Contains(columnId))
    {
        lockedColumns.Remove(columnId);
    }
}

有過面向對象編程經驗的同窗都知道,lockedColumns其實是Session中的一個對象引用,所以下面對此對象的 Add 和 Remove 操做會直接改變 Session 中的對象。

爲何 .Net Core 下,這個邏輯就失效了?

 

我第一個想到的是深拷貝,莫非下面的代碼返回了一個 Session 對象的深拷貝?

HttpContext.Session.GetObject<List<string>>(KEY_FOR_DATASOURCE_SESSION)

 

轉到 GetObject 方法的定義,我卻發現本身的忘性有多大,卻原來 GetObject 是本身好久以前定義的一個擴展方法,.Net Core自己並無定義這個方法,咱們來看一眼:

using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;

namespace FineUICore
{
    /// <summary>
    /// Session擴展
    /// </summary>
    public static class SessionExtension
    {
        /// <summary>
        /// 設置Session對象
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="session"></param>
        /// <param name="key"></param>
        /// <param name="obj"></param>
        public static void SetObject<T>(this ISession session, string key, T obj)
        {
            session.SetString(key, JsonConvert.SerializeObject(obj));
        }

        /// <summary>
        /// 獲取Session對象
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="session"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        public static T GetObject<T>(this ISession session, string key)
        {
            T result = default(T);
            var value = session.GetString(key);
            if(!String.IsNullOrEmpty(value))
            {
                result = JsonConvert.DeserializeObject<T>(value);
            }
            return result;
        }

    }
}

 

爲何 Session 中保存個對象還要經過JSON字符串中轉?

原來 .Net Core 中原生只提供了在 Session 中保存字符串和 byte 數組的支持,想要保存複雜類型,只能本身寫擴展方法了。

而這個擴展方法 GetObject 返回的Session對象的確像是一個深度拷貝的對象,所以對於它的 Add 和 Remove 並不會影響 Session 中實際存儲的 JSON字符串。

 

至此,問題已經很明朗了,咱們再來複習下 ASP.NET Core 中使用 Session 的步驟:

1. 首先在 Startup.cs 中添加 Session 服務

public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedMemoryCache();
    services.AddSession();
    
    // FineUI 和 MVC 服務
    services.AddFineUI(Configuration);
    services.AddMvc(options =>
    {
        // 自定義模型綁定(Newtonsoft.Json)
        options.ModelBinderProviders.Insert(0, new JsonModelBinderProvider());
    });


}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseStaticFiles();   
    app.UseSession();

    // FineUI 和 MVC 中間件(確保 UseFineUI 位於 UseMvc 的前面)
    app.UseFineUI();            
    app.UseMvc();
}

 

2. 控制器中使用 HttpContext.Session.SetString 來保存字符串

HttpContext.Session.SetString("StartedTime", "Started time:" + DateTime.Now.ToString());

var startedTime = HttpContext.Session.GetString("StartedTime");

 

若是咱們看下 SetString 的定義,會知道甚至這個方法也是經過 Microsoft.AspNetCore.Http 裏面定義的擴展方法提供的:

 

解決

知道了根本緣由,再去修正 FineUICore(ASP.NET Core)下的這個問題就簡單多了。

在控制器方法中,修改完 lockedColumns 對象後,須要顯式的保存到 Session 中,以下所示:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Grid1_ColumnLockUnlock(string type, string columnId)
{
    // 模擬操做數據庫中的數據
    List<string> lockedColumns = GetLockedColumns();
    if (type == "lock")
    {
        if (!lockedColumns.Contains(columnId))
        {
            lockedColumns.Add(columnId);
        }
    }
    else if (type == "unlock")
    {
        if (lockedColumns.Contains(columnId))
        {
            lockedColumns.Remove(columnId);
        }
    }
    
    HttpContext.Session.SetObject(KEY_FOR_DATASOURCE_SESSION, lockedColumns);

    return UIHelper.Result();
}

 

喜歡三石和他的文章,就加入[三石和他的朋友們]知識星球,能夠下載 FineUICore(基礎版),下載後永久商用,可運行於Linux,macOS,Windows。

相關文章
相關標籤/搜索