MVC 之 缓存机制(二)

  八、应用程序缓存

  应用程序缓存提供了一种编程方式,可通过键/值对将任意数据存储在内存中。 使用应用程序缓存与使用应用程序状态类似。 但是,与应用程序状态不同的是,应用程序缓存中的数据是易失的, 即数据并不是在整个应用程序生命周期中都存储在内存中。 使用应用程序缓存的优点是由 ASP.NET 管理缓存,它会在项过期、无效、或内存不足时移除缓存中的项。 还可以配置应用程序缓存,以便在移除项时通知应用程序。

  应用程序缓存的使用模式:确定在访问某一项时该项是否存在于缓存中,如果存在,则使用。 如果该项不存在,则可以重新创建该项,然后将其放回缓存中。 这一模式可确保缓存中始终有最新的数据。

  Catch 密封类 及 相关枚举类 在“ using System.Web.Caching; ”命名空间下。在 Asp.Net 中,System.Web 命名空间下的 HttpRuntime.Cache 以及 HttpContext.Current.Cache 都是该类的实例。

  1、Catch 类代码

#region 程序集 System.Web.dll, v4.0.30319
// C:Program Files (x86)Reference AssembliesMicrosoftFramework.NETFrameworkv4.0System.Web.dll
#endregion

using System;
using System.Collections;
using System.Reflection;
using System.Runtime;

namespace System.Web.Caching
{
    // 摘要:
    //     实现用于 Web 应用程序的缓存。此类不能被继承。
    public sealed class Cache : IEnumerable
    {
        // 摘要:
        //     用于 System.Web.Caching.Cache.Insert(System.String,System.Object) 方法调用中的 absoluteExpiration
        //     参数中以指示项从不到期。此字段为只读。
        public static readonly DateTime NoAbsoluteExpiration;
        //
        // 摘要:
        //     用作 System.Web.Caching.Cache.Insert(System.String,System.Object) 或 System.Web.Caching.Cache.Add(System.String,System.Object,System.Web.Caching.CacheDependency,System.DateTime,System.TimeSpan,System.Web.Caching.CacheItemPriority,System.Web.Caching.CacheItemRemovedCallback)
        //     方法调用中的 slidingExpiration 参数,以禁用可调到期。此字段为只读。
        public static readonly TimeSpan NoSlidingExpiration;

        // 摘要:
        //     初始化 System.Web.Caching.Cache 类的新实例。
        [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
        public Cache();

        // 摘要:
        //     获取存储在缓存中的项数。
        //
        // 返回结果:
        //     存储在缓存中的项数。
        public int Count { get; }
        //
        // 摘要:
        //     获取在 ASP.NET 开始从缓存中移除项之前应用程序可使用的物理内存百分比。
        //
        // 返回结果:
        //     可供应用程序使用的物理内存百分比。
        public long EffectivePercentagePhysicalMemoryLimit { get; }
        //
        // 摘要:
        //     获取可用于缓存的字节数。
        //
        // 返回结果:
        //     可用于缓存的字节数。
        public long EffectivePrivateBytesLimit { get; }

        // 摘要:
        //     获取或设置指定键处的缓存项。
        //
        // 参数:
        //   key:
        //     表示缓存项的键的 System.String 对象。
        //
        // 返回结果:
        //     指定的缓存项。
        public object this[string key] { get; set; }

        // 摘要:
        //     将指定项添加到 System.Web.Caching.Cache 对象,该对象具有依赖项、到期和优先级策略以及一个委托(可用于在从 Cache 移除插入项时通知应用程序)。
        //
        // 参数:
        //   key:
        //     用于引用该项的缓存键。
        //
        //   value:
        //     要添加到缓存的项。
        //
        //   dependencies:
        //     该项的文件依赖项或缓存键依赖项。当任何依赖项更改时,该对象即无效,并从缓存中移除。如果没有依赖项,则此参数包含 null。
        //
        //   absoluteExpiration:
        //     所添加对象将到期并被从缓存中移除的时间。如果使用可调到期,则 absoluteExpiration 参数必须为 System.Web.Caching.Cache.NoAbsoluteExpiration。
        //
        //   slidingExpiration:
        //     最后一次访问所添加对象时与该对象到期时之间的时间间隔。如果该值等效于 20 分钟,则对象在最后一次被访问 20 分钟之后将到期并从缓存中移除。如果使用绝对到期,则
        //     slidingExpiration 参数必须为 System.Web.Caching.Cache.NoSlidingExpiration。
        //
        //   priority:
        //     对象的相对成本,由 System.Web.Caching.CacheItemPriority 枚举表示。缓存在退出对象时使用该值;具有较低成本的对象在具有较高成本的对象之前被从缓存移除。
        //
        //   onRemoveCallback:
        //     在从缓存中移除对象时所调用的委托(如果提供)。当从缓存中删除应用程序的对象时,可使用它来通知应用程序。
        //
        // 返回结果:
        //     如果添加的项之前存储在缓存中,则为表示该项的对象;否则为 null。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     key 或 value 参数设置为 null。
        //
        //   System.ArgumentOutOfRangeException:
        //     slidingExpiration 参数设置成小于 TimeSpan.Zero 或大于一年。
        //
        //   System.ArgumentException:
        //     为要添加到 Cache 中的项设置 absoluteExpiration 和 slidingExpiration 参数。
        public object Add(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback);
        //
        // 摘要:
        //     从 System.Web.Caching.Cache 对象检索指定项。
        //
        // 参数:
        //   key:
        //     要检索的缓存项的标识符。
        //
        // 返回结果:
        //     检索到的缓存项,未找到该键时为 null。
        public object Get(string key);
        //
        // 摘要:
        //     检索用于循环访问包含在缓存中的键设置及其值的字典枚举数。
        //
        // 返回结果:
        //     要循环访问 System.Web.Caching.Cache 对象的枚举数。
        public IDictionaryEnumerator GetEnumerator();
        //
        // 摘要:
        //     向 System.Web.Caching.Cache 对象插入项,该项带有一个缓存键引用其位置,并使用 System.Web.Caching.CacheItemPriority
        //     枚举提供的默认值。
        //
        // 参数:
        //   key:
        //     用于引用该项的缓存键。
        //
        //   value:
        //     要插入缓存中的对象。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     key 或 value 参数为 null。
        public void Insert(string key, object value);
        //
        // 摘要:
        //     向 System.Web.Caching.Cache 中插入具有文件依赖项或键依赖项的对象。
        //
        // 参数:
        //   key:
        //     用于标识该项的缓存键。
        //
        //   value:
        //     要插入缓存中的对象。
        //
        //   dependencies:
        //     所插入对象的文件依赖项或缓存键依赖项。当任何依赖项更改时,该对象即无效,并从缓存中移除。如果没有依赖项,则此参数包含 null。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     key 或 value 参数为 null。
        public void Insert(string key, object value, CacheDependency dependencies);
        //
        // 摘要:
        //     向 System.Web.Caching.Cache 中插入具有依赖项和到期策略的对象。
        //
        // 参数:
        //   key:
        //     用于引用该对象的缓存键。
        //
        //   value:
        //     要插入缓存中的对象。
        //
        //   dependencies:
        //     所插入对象的文件依赖项或缓存键依赖项。当任何依赖项更改时,该对象即无效,并从缓存中移除。如果没有依赖项,则此参数包含 null。
        //
        //   absoluteExpiration:
        //     所插入对象将到期并被从缓存中移除的时间。要避免可能的本地时间问题(例如从标准时间改为夏时制),请使用 System.DateTime.UtcNow
        //     而不是 System.DateTime.Now 作为此参数值。如果使用绝对到期,则 slidingExpiration 参数必须为 System.Web.Caching.Cache.NoSlidingExpiration。
        //
        //   slidingExpiration:
        //     最后一次访问所插入对象时与该对象到期时之间的时间间隔。如果该值等效于 20 分钟,则对象在最后一次被访问 20 分钟之后将到期并被从缓存中移除。如果使用可调到期,则
        //     absoluteExpiration 参数必须为 System.Web.Caching.Cache.NoAbsoluteExpiration。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     key 或 value 参数为 null。
        //
        //   System.ArgumentOutOfRangeException:
        //     将 slidingExpiration 参数设置为小于 TimeSpan.Zero 或大于一年的等效值。
        //
        //   System.ArgumentException:
        //     为要添加到 Cache 中的项设置 absoluteExpiration 和 slidingExpiration 参数。
        public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration);
        //
        // 摘要:
        //     将对象与依赖项、到期策略以及可用于在从缓存中移除项之前通知应用程序的委托一起插入到 System.Web.Caching.Cache 对象中。
        //
        // 参数:
        //   key:
        //     用于引用对象的缓存键。
        //
        //   value:
        //     要插入到缓存中的对象。
        //
        //   dependencies:
        //     该项的文件依赖项或缓存键依赖项。当任何依赖项更改时,该对象即无效,并从缓存中移除。如果没有依赖项,则此参数包含 null。
        //
        //   absoluteExpiration:
        //     所插入对象将到期并被从缓存中移除的时间。要避免可能的本地时间问题(例如从标准时间改为夏时制),请使用 System.DateTime.UtcNow
        //     而不是 System.DateTime.Now 作为此参数值。如果使用绝对到期,则 slidingExpiration 参数必须设置为 System.Web.Caching.Cache.NoSlidingExpiration。
        //
        //   slidingExpiration:
        //     缓存对象的上次访问时间和对象的到期时间之间的时间间隔。如果该值等效于 20 分钟,则对象在最后一次被访问 20 分钟之后将到期并被从缓存中移除。如果使用可调到期,则
        //     absoluteExpiration 参数必须设置为 System.Web.Caching.Cache.NoAbsoluteExpiration。
        //
        //   onUpdateCallback:
        //     从缓存中移除对象之前将调用的委托。可以使用它来更新缓存项并确保缓存项不会从缓存中移除。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     key、value 或 onUpdateCallback 参数为 null。
        //
        //   System.ArgumentOutOfRangeException:
        //     将 slidingExpiration 参数设置为小于 TimeSpan.Zero 或大于一年的等效值。
        //
        //   System.ArgumentException:
        //     为要添加到 Cache 中的项设置 absoluteExpiration 和 slidingExpiration 参数。- 或 -dependencies
        //     参数为 null,absoluteExpiration 参数设置为 System.Web.Caching.Cache.NoAbsoluteExpiration
        //     并且 slidingExpiration 参数设置为 System.Web.Caching.Cache.NoSlidingExpiration。
        public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemUpdateCallback onUpdateCallback);
        //
        // 摘要:
        //     向 System.Web.Caching.Cache 对象中插入对象,后者具有依赖项、到期和优先级策略以及一个委托(可用于在从 Cache 移除插入项时通知应用程序)。
        //
        // 参数:
        //   key:
        //     用于引用该对象的缓存键。
        //
        //   value:
        //     要插入缓存中的对象。
        //
        //   dependencies:
        //     该项的文件依赖项或缓存键依赖项。当任何依赖项更改时,该对象即无效,并从缓存中移除。如果没有依赖项,则此参数包含 null。
        //
        //   absoluteExpiration:
        //     所插入对象将到期并被从缓存中移除的时间。要避免可能的本地时间问题(例如从标准时间改为夏时制),请使用 System.DateTime.UtcNow
        //     而不是 System.DateTime.Now 作为此参数值。如果使用绝对到期,则 slidingExpiration 参数必须为 System.Web.Caching.Cache.NoSlidingExpiration。
        //
        //   slidingExpiration:
        //     最后一次访问所插入对象时与该对象到期时之间的时间间隔。如果该值等效于 20 分钟,则对象在最后一次被访问 20 分钟之后将到期并被从缓存中移除。如果使用可调到期,则
        //     absoluteExpiration 参数必须为 System.Web.Caching.Cache.NoAbsoluteExpiration。
        //
        //   priority:
        //     该对象相对于缓存中存储的其他项的成本,由 System.Web.Caching.CacheItemPriority 枚举表示。该值由缓存在退出对象时使用;具有较低成本的对象在具有较高成本的对象之前被从缓存移除。
        //
        //   onRemoveCallback:
        //     在从缓存中移除对象时将调用的委托(如果提供)。当从缓存中删除应用程序的对象时,可使用它来通知应用程序。
        //
        // 异常:
        //   System.ArgumentNullException:
        //     key 或 value 参数为 null。
        //
        //   System.ArgumentOutOfRangeException:
        //     将 slidingExpiration 参数设置为小于 TimeSpan.Zero 或大于一年的等效值。
        //
        //   System.ArgumentException:
        //     为要添加到 Cache 中的项设置 absoluteExpiration 和 slidingExpiration 参数。
        public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback);
        //
        // 摘要:
        //     从应用程序的 System.Web.Caching.Cache 对象移除指定项。
        //
        // 参数:
        //   key:
        //     要移除的缓存项的 System.String 标识符。
        //
        // 返回结果:
        //     从 Cache 移除的项。如果未找到键参数中的值,则返回 null。
        public object Remove(string key);
    }
}
Cache 类

  2、通过键和值直接设置项向缓存赋值

List<MODEL.Classes> list = null;
if (Cache["myDog"] == null)
{
  Cache["myDog"] = new Dog() { name= "小瑞瑞", type= "柯基犬" };
  Response.Write("保存了一只狗狗");
  //查询数据库 存入缓存
  list = new BLL.Classes().GetList();
  Cache["list"] =  list;
}
else
{
  Dog myDog = Cache["myDog"] as Dog;
  Response.Write("myDog="+myDog.name);
  //从缓存里获取数据
  list = Cache["list"] as List<MODEL.Classes>;
}
//myDog, list 就可以使用了

  3、Insert 的方式加入缓存

  在 Cache 类代码中可以看到,Insert 方法有五重重载,最简单方式如下:

Cache.Insert("key", "value");

  (1) 添加依赖项(其他已存在的缓存;已存在的文件)

//依赖其他缓存的缓存
Cache.Insert("key1", "value1");
Cache.Insert("key2", "value2",new CacheDependency(null, new string[]{"key1"})); //依赖于key为key1的缓存
//依赖文件的缓存
Cache.Insert("key3", "value3",new CacheDependency(Server.MapPath("路径")));

  (2) 插入多依赖缓存

CacheDependency dep1 = new CacheDependency(null, new string[] { "key1" });
CacheDependency dep2 = new CacheDependency(Server.MapPath("路径"));
AggregateCacheDependency cacheList = new AggregateCacheDependency();
cacheList.Add(dep1);
cacheList.Add(dep2);
Cache.Insert("key", "value",cacheList);

  4、Add的形式添加缓存(该方法没有重载)

  (CacheItemPriority 枚举、CacheItemRemovedReason 枚举)在 System.Web.Caching 命名空间下,可添加移除回调。

Cache.Add("key", "value", null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, null);

  5、绝对过期时间和滑动过期时间

  绝对过期字段的英文是NoSlidingExpiration(不滑动),滑动过期的英文是NoAbsoluteExpiration(不绝对)。

//绝对过期时间的时间为30s,过完30s之后就没有缓存
Cache.Insert("key1", "value1",null, DateTime.Now.AddSeconds(30), Cache.NoSlidingExpiration,CacheItemPriority.High, RemoveCall);
//滑动过期时间的时间为1分钟,只要一直刷新一直有,如果刷新间隔超过一分钟的缓存就没啦
Cache.Insert("key2", "value2", null, Cache.NoAbsoluteExpiration,new TimeSpan(0, 0, 60));
public void RemoveCall(string key, object value, CacheItemRemovedReason call)
{
    if (key=="key1")
    {
        Cache.Remove("key1");
    }
}

  九、可拓展缓存(自定义)

  ASP.NET 1到3.5中的输出缓存有一个限制,就是缓存存储本身不是可扩展的,ASP.NET 4.0之后是支持缓存扩展的。 通过缓存的扩展,可以保存HTML页面或者内存中的数据, 这些存储选项包括本地或远程磁盘、云存储和分布式缓存引擎。ASP.NET 中的输出缓存提供程序扩展性,可以为网站设计更好的输出缓存策略。实现可扩展,需要创建一个继承自 System.Web.Caching 命名空间下的 OutputCacheProvider 的抽象基类。 

  例如,可以创建这样一个输出缓存提供程序,该程序在内存中缓存站点流量排名靠前的页面,而在磁盘上缓存流量较低的页面,也可以对所呈现页面的各种变化因素组合进行缓存,但应使用分布式缓存以减少前端 Web 服务器的内存消耗。

    public class MyCacheProvide : OutputCacheProvider
    {
        public override object Add(string key, object entry, DateTime utcExpiry)
        {
            throw new NotImplementedException();
        }

        public override object Get(string key)
        {
            throw new NotImplementedException();
        }

        public override void Remove(string key)
        {
            throw new NotImplementedException();
        }

        public override void Set(string key, object entry, DateTime utcExpiry)
        {
            throw new NotImplementedException();
        }
    }

  然后配置文件中进行相应配置:

    <caching>
      <outputCache defaultProvider="AspNetInternalProvider">
        <providers>
          <add name="DiskCache" type="MyWeb.Controllers.MyCacheProvide, MyWeb" />
        </providers>
      </outputCache>
    </caching>

  默认情况下,所有 HTTP 响应、所呈现的页面和控件(其中 defaultProvider 特性设置为 AspNetInternalProvider)所示的内存输出缓存。通过为 defaultProvider 指定不同的提供程序名称,可以更改用于 Web 应用程序的默认输出缓存提供程序。默认值为“AspNetInternalProvider", 这是 ASP.NET 提供的内存缓存。

  十、MVC 缓存问题

  1、在 ASP.NET MVC 3 中如果使用了 OutputCache,一定要在 Action 中添加下面的代码,切记!

Response.Cache.SetOmitVaryStar(true);

  这是一个伴随ASP.NET从1.0到4.0的OutputCache Bug,ASP.NET MVC 3 是基于 ASP.NET 4.0 的,所以也躲不过。

  问题演示:

  下面先来体验一下不加 Response.Cache.SetOmitVaryStar(true); 的情况。

  示例Action代码:

[OutputCache(Duration = 120)]
public ActionResult SiteHome(int? pageIndex)
{
    ...
}

  注意:OutputCache.Location的默认值是OutputCacheLocation.Any(服务端、客户端、代理服务器端等都进行缓存)

  第一次请求:

  

  第二次请求(F5刷新浏览器):

  

  第三次请求(F5刷新浏览器):

  

  接着第四次请求会返回304,第五次请求又返回200。。。

  再体验一下加“ Response.Cache.SetOmitVaryStar(true); ”的情况。

[OutputCache(Duration = 120)]
public ActionResult SiteHome(int? pageIndex)
{
    Response.Cache.SetOmitVaryStar(true);
    ...
}

  第一次请求:

  

  第二次请求(F5刷新浏览器):

  

  第三次请求(F5刷新浏览器):

  

  注:只要在缓存有效期内,服务器一直返回304。

  问题分析

  1. 200与304的区别

  当返回状态码是200时,服务器端会将当前请求的整个页面全部发送给客户端(消耗下行带宽)。

  当返回状态码是304时,由于客户端浏览器提供的 Last-Modified 时间在服务器端的缓存有效期内,服务器端只发送这个状态码,不发送页面的任何内容(几乎不消耗下行带宽),浏览器直接从本地缓存中获取内容。

  所以,304的好处就是节约带宽,响应速度更快。

  2. 对服务端缓存的影响

  加不加 Response.Cache.SetOmitVaryStar(true),服务端的缓存情况都是一样的。只是不加 SetOmitVaryStar(true) 时,对于同一个客户端浏览器,每隔一次请求,服务器端就不管客户端浏览器的缓存,重新发送页面内容,但是只要在缓存有效期内,内容还是从服务器端缓存中读取。

  问题危害

  ASP.NET 缓存的这个诡异行为,导致在不知不觉中浪费了带宽资源。

  十一、MVC4 缓存情况实例对比

  1、未加缓存情况

[HttpPost]
public ActionResult Left(string parentId)
{
  ...... 
}

  

  2、添加缓存情况

[HttpPost]
[OutputCache(Duration = 10, VaryByParam = "*")]
public ActionResult Left(string parentId)
{
  ...... 
}

  

  3、添加缓存并添加 Response.Cache.SetOmitVaryStar(true);  情况

  若在 MVC4 及以上版本中使用  Response.Cache.SetOmitVaryStar(true);  ,[OutputCache(Duration = 10)]中设置的 Duration 属性将失效,过期时间有效值为“ max-age=63625076997 ”秒。如下:

[HttpPost]
[OutputCache(Duration = 10)]
public ActionResult Left(string parentId)
{
    //Response.Cache.SetOmitVaryStar(true); //过期时间“max-age=63625076997”秒
}

  

   十二、ASP.NET MVC Action Filter - 缓存与压缩

  我们可以将HTTP请求在一个定义的时间内缓存在用户的浏览器中,如果用户在定义的时间内请求同一个URL,那么用户的请求将会从用户浏览器的缓存中加载,而不是从服务器。可以在ASP.NET MVC应用程序中使用下面的Action Filter来实现同样的事情:

using System;
using System.Web;
using System.Web.Mvc;

public class CacheFilterAttribute : ActionFilterAttribute
{
    /// <summary>
    /// Gets or sets the cache duration in seconds. The default is 10 seconds.
    /// </summary>
    /// <value>The cache duration in seconds.</value>
    public int Duration
    {
        get;
        set;
    }

    public CacheFilterAttribute()
    {
        Duration = 10;
    }

    public override void OnActionExecuted(FilterExecutedContext filterContext)
    {
        if (Duration <= 0) return;

        HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
        TimeSpan cacheDuration = TimeSpan.FromSeconds(Duration);

        cache.SetCacheability(HttpCacheability.Public);
        cache.SetExpires(DateTime.Now.Add(cacheDuration));
        cache.SetMaxAge(cacheDuration);
        cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
    }
}

  可以像下面一样在你的Controller Action 方法中使用这个Filter :

[CacheFilter(Duration = 60)]
public void Category(string name, int? page)

  下面是在firebug中当 缓存Filter 没有应用的时候的截图 :

  NoCache

  下面的截图是应用了 Cache Filter 时候的截图 :

  Cache

   另外一个很重要的事情就是压缩。现在的浏览器都可以接收压缩后的内容,这可以节省大量的带宽。你可以在你的ASP.NET MVC 程序中应用下面的Action Filter 来压缩你的Response :

using System.Web;
using System.Web.Mvc;

public class CompressFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(FilterExecutingContext filterContext)
    {
        HttpRequestBase request = filterContext.HttpContext.Request;

        string acceptEncoding = request.Headers["Accept-Encoding"];

        if (string.IsNullOrEmpty(acceptEncoding)) return;

        acceptEncoding = acceptEncoding.ToUpperInvariant();

        HttpResponseBase response = filterContext.HttpContext.Response;

        if (acceptEncoding.Contains("GZIP"))
        {
            response.AppendHeader("Content-encoding", "gzip");
            response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
        }
        else if (acceptEncoding.Contains("DEFLATE"))
        {
            response.AppendHeader("Content-encoding", "deflate");
            response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
        }
    }
}

  然后将这个Filter应用到你的Controller Action 中 :

[CompressFilter]
public void Category(string name, int? page)

  下面是没有应用压缩的时候的截图 :

  Uncompressed

  下面的截图是应用了压缩Filter后的情形 :

  Compressed

  当然也可以将这两个Filter都应用到同一个Action方法上,就好像下面所示 :

[CompressFilter(Order = 1)]
[CacheFilter(Duration = 60, Order = 2)]
public void Category(string name, int? page)

  下面是截图 :

  Both

原文地址:https://www.cnblogs.com/xinaixia/p/6553063.html