官术网_书友最值得收藏!

Maintaining the state

What if you need to maintain a state, either from one component to the other in the same request, or across requests? Web applications have traditionally offered solutions for this. Let's explore the options we have.

Using the request

Any object that you store in the request (in memory) will be available throughout its duration. Items are a strongly typed dictionary in the HttpContext class:

this.HttpContext.Items["timestamp"] = DateTime.UtcNow;

You can check for the existence of the item before accessing it; it is worth noting that the following is case sensitive:

if (this.HttpContext.Items.ContainsKey("timestamp")) { ... }

Of course, you can also remove an item:

this.HttpContext.Items.Remove("timestamp");

Using form data

The Form collection keeps track of all values submitted by an HTML FORM, normally after a POST request. To access it, you use the Form property of the Request object of HttpContext:

var isChecked = this.HttpContext.Request.Form["isChecked"].Equals("on");

You can program defensively by first checking for the existence of the value (case insensitive):

if (this.HttpContext.Request.Form.ContainsKey("isChecked")) { ... }

It is possible to obtain multiple values, and in this case, you can count them and get all their values:

var count = this.HttpContext.Request.Form["isChecked"].Count;
var values = this.HttpContext.Request.Form["isChecked"].ToArray();

Using the query string

Usually, you won't store data in the query string, but will instead get data from it—for example, http://servername.com?isChecked=true. The Query collection keeps track of all parameters that are sent in the URL as strings:

var isChecked = this.HttpContext.Request.Query["isChecked"].Equals("true");

To check for the presence of a value, we use the following:

if (this.HttpContext.Request.Query.ContainsKey("isChecked")) { ... }

This also supports multiple values:

var count = this.HttpContext.Request.Query["isChecked"].Count;
var values = this.HttpContext.Request.Query["isChecked"].ToArray();

Using the route

As with the query string approach, you typically only get values from the route and do not write to them; however, you do have methods in the IUrlHelper interface—which is normally accessible through the Url property of the ControllerBase class—that generate action URLs, from which you can pack arbitrary values.

Route parameters look like http://servername.com/admin/user/121, and use a route template of [controller]/[action]/{id}.

To get a route parameter (a string), you do the following:

var id = this.RouteData.Values["id"];

To check that it's there, use the following:

if (this.RouteData.ContainsKey("id")) { ... }

Using cookies

Cookies have been around for a long time and are the basis of a lot of functionality on the web, such as authentication and sessions. They are specified in RFC 6265 (https://tools.ietf.org/html/rfc6265). Essentially, they are a way of storing small amounts of text in the client.

You can both read and write cookies. To read a cookie value, you only need to know its name; its value will come as a string:

var username = this.HttpContext.Request.Cookies["username"];

Of course, you can also check that the cookie exists with the following:

if (this.HttpContext.Request.Cookies.ContainsKey("username")) { ... }

To send a cookie to the client as part of the response, you need a bit more information, namely the following:

  • Name (string): A name (what else?)
  • Value (string): A string value
  • Expires (DateTime): An optional expiration timestamp (the default is for the cookie to be session-based, meaning that it will vanish once the browser closes)
  • Path (string): An optional path from which the cookie is to be made available (the default is /)
  • Domain (string): An optional domain (the default is the current fully qualified hostname)
  • Secure (bool): An optional secure flag that, if present, will cause the cookie to only be available if the request is being served using HTTPS (the default is false)
  • HttpOnly (bool): Another optional flag that indicates whether the cookie will be readable by JavaScript on the client browser (the default is also false)

We add a cookie to the request object as follows:

this.HttpContext.Response.Cookies.Append("username", "rjperes", new CookieOptions
{
Domain = "packtpub.com",
Expires = DateTimeOffset.Now.AddDays(1),
HttpOnly = true,
Secure = true,
Path = "/"
});

The third parameter, of the CookieOptionstypeis optional, in which case the cookie assumes the default values.

The only way you can revoke a cookie is by adding one with the same name and an expiration date in the past.

You mustn't forget that there is a limit to the number of cookies you can store per domain, as well as a limit to the actual size of an individual cookie value; these shouldn't be used for large amounts of data. For more information, please consult RFC 6265.

Using sessions

Sessions are a way to persist data per client. Typically, sessions rely on cookies, but it's possible (yet error prone) to use query string parameters, and ASP.NET Core does not support this out of the box. In ASP.NET Core, sessions are opt-in; in other words, they need to be explicitly added. We need to add the NuGet package Microsoft.AspNetCore.Session and explicitly add support in the Configure and ConfigureServices methods of the Startup class:

public void ConfigureServices(IServiceCollection services)
{
services.AddSession();
//rest goes here
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSession();
//rest goes here
}

After that, the Session object is made available in the HttpContext instance:

var value = this.HttpContext.Session.Get("key");  //byte[]

A better approach is to use the GetString extension method and serialize/deserialize to JSON:

var json = this.HttpContext.Session.GetString("key");
var model = JsonSerializer.Deserialize<Model>(json);

Here, Model is just a POCO class and JsonSerializer is a class from System.Text.Json that has static methods for serializing and deserializing to and from JSON strings.

To store a value in the session, we use the Set or SetString methods:

this.HttpContext.Session.Set("key", value); //value is byte[]

The JSON approach is as follows:

var json = JsonSerializer.Serialize(model);
this.HttpContext.Session.SetString("key", json);

Removal is achieved by either setting the value to null or calling Remove. Similar to GetString and SetString, there are also the GetInt32 and SetInt32 extension methods. Use what best suits your needs, but never forget that the data is always stored as a byte array.

If you want to check for the existence of a value in the session, you should use the TryGetValue method:

byte[] data;
if (this.HttpContext.Session.TryGetValue("key", out data)) { ... }

That's pretty much it for using the session as a general-purpose dictionary. Now it's, configuration time! You can set some values, mostly around the cookie that is used to store the session, plus the idle interval, in a SessionOptions object:

services.AddSession(options =>
{
options.CookieDomain = "packtpub.com";
options.CookieHttpOnly = true;
options.CookieName = ".SeSsIoN";
options.CookiePath = "/";
options.CookieSecure = true;
options.IdleTimeout = TimeSpan.FromMinutes(30);
});

These can also be configured in the UseSession method in Configure:

app.UseSession(new SessionOptions { ... });

One final thing to note is that a session, by default, will use in-memory storage, which won't make it overly resilient or useful in real-life apps; however, if a distributed cache provider is registered before the call to AddSession, the session will use that instead! So, let's take a look at the next topic to see how we can configure it.

Before moving on, we need to keep in mind the following:

  • There's a bit of a performance penalty in storing objects in the session.
  • An object may be evicted from the session if the idle timeout is reached.
  • Accessing an object in the session prolongs its lifetime—that is, its idle timeout is reset.

Using the cache

Unlike previous versions of ASP.NET, there is no longer built-in support for the cache; like most things in .NET Core, it is still available but as a pluggable service. There are essentially two kinds of cache in .NET Core:

  • In-memory cache, which is represented by the IMemoryCache interface
  • Distributed cache, which uses the IDistributedCache interface

ASP .NET Core includes a default implementation of IMemoryCache as well as one for IDistributedCache. The caveat for the distributed implementation is that it is also in-memory—it is only meant to be used in testing, but the good thing is that there are several implementations available, such as Redis (https://redis.io/) or SQL Server.

In-memory and distributed caches can be used simultaneously, as they are unaware of each other.

Both the distributed and in-memory cache store instances as byte arrays (byte[]) but a good workaround is to first convert your objects to JSON and then use the method extensions that work with strings as follows:

var json = JsonSerializer.Serialize(model);
var model = JsonSerializer.Deserialize<Model>(json);
In-memory cache

In order to use the in-memory cache, you need to register its service in ConfigureServices using the following default options:

services.AddMemoryCache();

If you prefer, you can also fine-tune them by using the overloaded extension method that takes a MemoryCacheOptions instance:

services.AddMemoryCache(options =>
{
options.Clock = new SystemClock();
options.CompactOnMemoryPressure = true;
options.ExpirationScanFrequency = TimeSpan.FromSeconds(5 * 60);
});

The purposes of these properties are as follows:

  • Clock (ISystemClock): This is an implementation of ISystemClock that will be used for the expiration calculation. It is useful for unit testing and mocking; there is no default.
  • CompactOnMemoryPressure (bool): This is used to remove the oldest objects from the cache when the available memory gets too low; the default is true.
  • ExpirationScanFrequency (TimeSpan): This sets the interval that .NET Core uses to determine whether to remove objects from the cache; the default is one minute.

In order to use the in-memory cache, we need to retrieve an instance of IMemoryCache from the dependency injection:

public IActionResult StoreInCache(Model model, [FromServices] IMemoryCache cache)
{
cache.Set("model", model);
return this.Ok();
}

We will look at [FromServices] in more detail in the Dependency injectionsection.

IMemoryCachesupports all the operations that you might expect, plus a few others:

  • CreateEntry: Creates an entry in the cache and gives you access to expiration
  • Get/GetAsync: Retrieves an item from the cache, synchronously or asynchronously
  • GetOrCreate/GetOrCreateAsync: Returns an item from the cache if it exists, or creates one, synchronously or asynchronously
  • Set/SetAsync: Adds or modifies an item in the cache, synchronously or asynchronously
  • Remove: Removes an item from the cache
  • TryGetValue: Tentatively tries to get an item from the cache, synchronously

That's pretty much it! The memory cache will be available for all requests in the same application and will go away once the application is restarted or stopped.

Distributed cache

The default out-of-the-box implementation of the distributed cache is pretty much useless in real-life scenarios, but it might be a good starting point. Here's how to add support for it in ConfigureServices:

services.AddDistributedMemoryCache();

There are no other options—it's just that. In order to use it, ask the Dependency Injection container for an instance of IDistributedCache:

private readonly IDistributedCache _cache;

public CacheController(IDistributedCache cache)
{
this._cache = cache;
}

public IActionResult Get(int id)
{
return this.Content(this._cache.GetString(id.ToString()));
}

The included implementation will behave in exactly the same ways as the in-memory cache, but there are also some good alternatives for a more serious use case. The API it offers does the following:

  • Get/GetAsync: Returns an item from the cache
  • Refresh/RefreshAsync: Refreshes an item in the cache, prolonging its lifetime
  • Remove/RemoveAsync: Removes an item from the cache
  • Set/SetAsync: Adds an item to the cache or modifies its current value

Be warned that because the cache is now distributed and may take some time to synchronize, an item that you store in it may not be immediately available to all clients.

Redis

Redis is an open source distributed cache system. Its description is beyond the scope of this book, but it's sufficient to say that Microsoft has made a client implementation available for it in the form of the Microsoft.Extensions.Caching.Redis NuGet package. After you add this package, you get a couple of extension methods that you need to use to register a couple of services in ConfigureServices, which replaces the Configuration and InstanceName properties with the proper values:

services.AddDistributedRedisCache(options =>
{
options.Configuration = "servername";
options.InstanceName = "Shopping";
});

And that's it! Now, whenever you ask for an instance of IDistributedCache, you will get one that uses Redis underneath.

There is a good introduction to Redis available at https://redis.io/topics/quickstart.
SQL Server

Another option is to use the SQL Server as a distributed cache.Microsoft.Extensions.Caching.SqlServeris the NuGet package that adds support for it. You can add support for it inConfigureServicesas follows:

services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = @"Server=.; Database=DistCache;
Integrated Security=SSPI;";
options.SchemaName = "dbo";
options.TableName = "Cache";
});

The rest is identical, so just get hold of IDistributedCache from the DI and off you go.

ASP.NET Core no longer includes the HttpApplication and HttpApplicationState classes, which is where you could keep state applications. This mechanism had its problems, and it's better if you rely on either an in-memory or distributed cache instead.

Using temporary data

The Controller class offers a TempData property of the ITempDataDictionarytype. Temporary data is a way of storing an item in a request so that it is still available in the next request. It's provider based, and there are currentlytwo providers available:

  • Cookie (CookieTempDataProvider)
  • Session (SessionStateTempDataProvider)

For the latter, you need to enable session state support. To do this, you pick one of the providers and register it using the dependency injection framework, normally in the ConfigureServices method:

//only pick one of these
//for cookies
services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
//for session
services.AddSingleton<ITempDataProvider, SessionStateTempDataProvider>();

Since ASP.NET Core 2, the CookieTempDataProvider is already registered. If you use SessionStateTempDataProvider, you also need to enable sessions.

After you have selected one of the providers, you can add data to the TempData collection:

this.TempData["key"] = "value";

Retrieving and checking the existence is trivial, as you can see in the following code:

if (this.TempData.ContainsKey("key"))
{
var value = this.TempData["key"];
}

After you have enabled temporary data by registering one of the providers, you can use the [SaveTempData] attribute. When applied to a class that is returned by an action result, it will automatically be kept in temporary data.

The [TempData] attribute, if applied to a property in the model class, will automatically persist the value for that property in temporary data:

[TempData]
public OrderModel Order { get; set; }

Comparing state maintenance techniques

The following table provides a simple comparison of all the different techniques that can be used to maintain the state among requests:

Needless to say, not all of these techniques serve the same purpose; instead, they are used in different scenarios.

In the next section, we will learn how to use dependency injection inside controllers.

主站蜘蛛池模板: 文安县| 东安县| 闽清县| 琼中| 东辽县| 手游| 宝鸡市| 林周县| 绩溪县| 武清区| 永兴县| 克山县| 高雄县| 南乐县| 凤山市| 巴青县| 恭城| 兴山县| 长子县| 成武县| 曲水县| 微博| 淮北市| 龙里县| 灯塔市| 盐亭县| 广河县| 宁化县| 咸宁市| 贡觉县| 辽宁省| 阿巴嘎旗| 贺州市| 新蔡县| 灵宝市| 宁德市| 建德市| 安吉县| 肇源县| 五原县| 曲周县|