zoukankan      html  css  js  c++  java
  • 你注意到 .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 中(实际项目中是要保存到数据库中的):

    [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 下的代码。

    FineUICore(ASP.NET Core)中的代码稍微不同,如下所示:

    [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中读取保存的列锁定状态:

    // 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。

  • 相关阅读:
    MySQL之pymysql模块
    MySQL 之 索引原理与慢查询优化
    MySQL 之 视图、触发器、存储过程、函数、事物与数据库锁
    MySql之数据操作
    MySQL之多表查询
    MySQL之单表查询
    MySQL之表的约束
    MySQL之表操作
    MySQL之表的数据类型
    pycharm 2016 注册(pycharm-professional-2016.3.2)
  • 原文地址:https://www.cnblogs.com/sanshi/p/10550977.html
Copyright © 2011-2022 走看看