- Hands-On Network Programming with C# and .NET Core
- Sean Burns
- 1688字
- 2021-06-24 16:05:29
Class properties
So, your constructors give you a clear idea of the context in which the classes are expected to be used, and your properties define the overall shape of a request. They define the clearest and most unambiguous description of what the class actually is. What can we learn about WebRequest, based on its properties? Well, let's take a closer look.
According to the base class specification, the public properties of the class are in alphabetical order ( as they're listed in the Microsoft documentation, here: https://docs.microsoft.com/en-us/dotnet/api/system.net.webrequest?view=netcore-3.0), as follows:
- AuthenticationLevel
- CachePolicy
- ConnectionGroupName
- ContentLength
- ContentType
- Credentials
- DefaultCachePolicy
- DefaultWebProxy
- Headers
- ImpersonationLevel
- Method
- PreAuthenticate
- Proxy
- RequestUri
- Timeout
- UseDefaultCredentials
So, what does this tell us about instances derived from this abstract class? Well, the obvious information is that it encapsulates common aspects of requests made over any protocol leveraged on the internet. Headers, Method (that is, the specific protocol method, such as a GET, POST, or PUT HTTP method), and RequestUri are all that you would expect from a utility class. Others, though, such as ImpersonationLevel, AuthenticationLevel, and CachePolicy indicate that more than simply encapsulating a request payload, the WebRequest class is truly meant to encapsulate an operation.
The actions of authenticating and caching responses fall outside of the responsibility of the simple-request payload and fall more into the segment of your software responsible for brokering requests and responses between your application and external resources. The presence of these methods indicates to us that this class (and its sub-classes) is intended to be the broker of requests and responses network resources. Its definition makes clear that it can handle the nitty-gritty details of connecting to remote hosts, authenticating itself, and, by extension, your application, serializing payloads, deserializing responses, and providing all of this through a clean and simple interface.
With the ContentType and ContentLength properties, it provides a clean way to access and set the values for the most commonly required headers for any request with a payload. The specification is telling you to just give me that package, tell me where you want to send it, and let me handle the rest. It even gives you an interface for lumping similar operations together in a connection group through the ConnectionGroupName property.
Imagine that you have multiple requests to the same external RESTful API living at https://financial-details.com/market/api, and there are a dozen different endpoints that your application accesses over the course of its runtime. Meanwhile, you also have a handful of requests that need to be routed to https://real-estate-details.com/market/api. You can simply associate all of the requests made to the financial details API under one connection group name, and the real estate details API requests under another. Doing so allows .NET to more reliably manage connections to a single ServicePoint instance. This allows multiple requests to a single endpoint to be routed over the same active connection, improving performance and reducing the risk of what's known as connection pool starvation.
Implementing this feature is quite trivial, but it can save you a mountain of time in performance tuning and chasing bugs. Simply define a static constant name for each connection group that you want to leverage, as follows:
namespace WebRequest_Samples
{
// Service class to encapsulate all external requests.
public class RequestService {
private static readonly string FINANCE_CONN_GROUP = "financial_connection";
private static readonly string REAL_ESTATE_CONN_GROUP = "real_estate_connection";
...
Then, whenever you need to instantiate a new request for the target endpoint, you can simply specify the connection group name through the assignment, and under the hood, the ServicePoint instance that is associated with the WebRequest instance will check for any connections that share the group name, and, if one is discovered, leverage the connection for your new request:
public static Task SubmitRealEstateRequest()
{
WebRequest req = WebRequest.Create("https://real-estate-detail.com/market/api");
req.ConnectionGroupName = REAL_ESTATE_CONN_GROUP;
...
}
And just like that, your request will take advantage of any established connections to that same external resource. If there are no other requests associated with the specified ConnectionGroupName property, then .NET Core will create a connection in its connection pool, and associate your request as the first in the connection group. This is especially useful if a set of requests are targeting a resource that requires access credentials, as the connection is established with those access credentials once, and then shared with the subsequent requests!
Once the connection is established, we'll need to know what to do with the responses for that request. For that, we have the CachePolicy property. This specifies how your requests should handle the availability of a cached response from your remote resource. This property gives us granular control over exactly how and when we should rely on a cached response, if at all. So, for example, if we have a dataset that is updated almost constantly, and we always want the most up-to-date response, we could avoid the cache entirely, by setting the policy accordingly:
using System.Net.Cache;
...
public static Task SubmitRealEstateRequest()
{
WebRequest req = WebRequest.Create("https://real-estate-detail.com/market/api");
req.ConnectionGroupName = REAL_ESTATE_CONN_GROUP;
var noCachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore);
req.CachePolicy = noCachePolicy;
...
}
And just like that, the request will ignore any available cached responses, and likewise, it won't cache whatever response it receives from the external resource itself. As you can see, the property expects an instance of a RequestCachePolicy object, which is typically initialized with a value from the RequestCacheLevel enum definition found in the System.Net.Cache namespace (as indicated by its inclusion at the top of the code block).
This is another instance where familiarizing yourself with the IntelliSense tools of Visual Studio can give you a clear idea of what values are available in that enum. Of course, if you're using something such as Visual Studio Code or another source code editor, you can always look up the source code or the documentation for it on the manufacture's website. No matter which editor you use, in the case of properties or methods whose use is not easy to infer, make a habit of looking up implementation details and notes on Microsoft's documentation. But with something as obvious and straightforward as an enum defining cache policies, Visual Studio's autocomplete and IntelliSense functionality can save you the time and mental energy of context-switching away from your IDE to look up valid values.
In the same way that you define the behavior around cached or cache-able responses, you can use the public properties of the WebRequest instance to define and specify the expected behavior for authentication of your application and any expectations you have of the remote resource to authenticate. This is exposed through the AuthenticationLevel property and behaves much the same way as the CachePolicy property that we just looked at.
Suppose, for instance, that your software depends on a remote resource that is explicitly configured to work with only your software. The remote server would need to authenticate requests to ensure that they are generated from valid instances of your software. Likewise, you will want to make sure that you are communicating directly with the properly configured server, and not some man-in-the-middle agent looking to swipe your valuable financial and real-estate details. In that case, you would likely want to ensure that every request is mutually authenticated, and I'm sure you can already see where I'm about to go with this.
Since the WebRequest class is designed to encapsulate the entire operation of interacting with remote resources, we should expect that we can configure our instance of that class with the appropriate authentication policies, and not have to manage it ourselves. And that's exactly what we can do. Building on our earlier example, we can define the AuthenticationLevel property to enforce the policy we want to use once, and then let the WebRequest instance take it from there:
using System.Net.Security;
...
public static Task SubmitRealEstateRequest()
{
WebRequest req = WebRequest.Create("https://real-estate-detail.com/market/api");
req.ConnectionGroupName = REAL_ESTATE_CONN_GROUP;
var noCachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore);
req.CachePolicy = noCachePolicy;
req.AuthenticationLevel = AuthenticationLevel.MutualAuthRequired;
...
}
Note the inclusion of the System.Net.Security namespace in our using directives. This is where the AuthenticationLevel enum is defined. This makes sense, as authentication is one-half of the authentication and authorization security components of most network software. But we'll get more into that later.
As you can guess, getting your own software authenticated will likely require some credentials.
Assigning credentials is as easy to do as defining your authentication or caching policies. In the WebRequest class definition, the Credentials property is an instance of the ICredentials interface from the System.Net namespace, typically implemented as an instance of the NetworkCredential class. Again, the full scope of implementing reliable security for network requests will be covered later in this book, but for now, let's take look at how we might add some credentials to our mutually- authenticated web requests. It uses the System.Net namespace, so no additional using statements are required. Instead, we can simply set the property to a new instance of NetworkCredential and move on, as follows:
req.Credentials = new NetworkCredential("test_user", "secure_and_safe_password");
We should actually be storing the password as SecureString, but this constructor is valid, and as I said, we'll look closer at security in later chapters.
With this short, straightforward example, we can clearly see how the class properties of WebRequest define the expected use case for instances of the concrete sub-classes that implement and extend it. Now that we understand the shape and scope of the operations WebRequest intends to abstract away for us, let's take a look at the actual execution of those operations through the public methods exposed by the class.