About
This project presents a Visual Studio solution including a simple demo ASP.Net Web API Service Application and a “Tester” Client (Windows Form Application) that allows the user to test the Web API with CRUD operations (GET, POST, PUT, DELETE). In addition to demonstrating standard CRUD capabilities, the Web API service implements a .Net Memory Cache (MemoryCache). The client “tester” application also allows the user to verify that the memory cache is implemented correctly and expiring per the set policy. Discussion of the memory cache implementation may help the reader with tips to understand, correctly implement, and verify the cache is expiring. Lastly, the project is shown in the demo section with a video and screen captures.
Architecture
The demo project consists of these simple component topics:
- Net Web API Service (Hosted by IIS Express) Application Project “WebAPIService”
- Client “Tester to Service” Windows Form Application Project “Client”
ASP.Net Web API Service Application
The ASP.Net Web Application template was used to create a simple Web API service and was hosted on my local development machine using IIS Express launched from Visual Studio. The API functions will be discussed after the Memory Cache approach. The code is available on GitHub [here].
Memory Cache “MemoryCache” .Net Discussion and Notes
Before I discuss the implementation code, I wanted to mention some tips and words of wisdom that I have learned working with the .Net MemoryCache in my Web API implementation [doc] that may help you in your project.
- When the client calls the Web API for any request, the service creates a unique controller object instance to service each of the client’s requests (similar to PerCall in WCF). The class will be setup for instance objects unless you otherwise mark it static. My Web API implementation, discussion, and notes assumes instances not a Singleton service.
- Because each Web API call spins up a new instance, the Values controller constructor is called every time the client calls the Web API (see note above). This affects the approach of implementing a memory cache.
- “MemoryCache.Default” is the default instance of the cache and can be shared by all client callers to the service [see doc]. So, in order to utilize the Memory Cache, the controller should create an object that references this default instance object. Here is a code example…
- Each client call to the Web API spins up a new Values controller object instance with its own cache object (newly created each call) that references the default “shared” cache.
- The Values controller constructor executes first before any API call (example: Get). We can verify if there is an existing key-value pair (like a Dictionary) in the default cache upon the constructor load.
- If the default cache does not contain the specified key (string) and value (can be object) for our inquiry, we first create one (key) and add it to the default cache with its value.
- We can add the key-value pair to the default cache with a policy object (Expiration) using the cache.Set method [see doc].
- Once the policy is set in the constructor to create the MemoryCache object…. And you’ve created the object… you cannot retrieve the policy object. There is not much you can do with a MemoryCache object to retrieve information about its previously set policy or when it’s going to expire after it has been constructed with a policy object (I think this would be a good feature enhancement to add a policy object as a property to the MemoryCache). I studied the current Microsoft (.Net Framework) docs to come to this conclusion, and if I’m wrong, please reach out to me and explain why and how you can retrieve the policy information from a MemoryCache object.
- Once the cache key-value pair is constructed with a policy, it is stuck with that policy until either it expires (the policy expiration property) or the key-value pair is modified with a new policy or modified without a specified policy object (defaults to non-expiring or when the App pool needs to recycle).
- Using the indexer method to set or modify your cache key-value … very important…. The expiration policy will be changed to an InfiniteAbsoluteExpiration [see doc]
- Using the cache.Set method to set or update the key-value in the cache allows for including a policy object that controls the expiration. Remember note # 8 above, means you have to create a brand-new policy object with your expiration settings. When you use the cache.Set method you are effectively resetting the policy of the existing key-value entry in the cache.
My MemoryCache Implementation Approach
My approach for implementing a Memory Cache was to create a cache object for each Web API call that points to the default cache (single instance). At the start of each client call to the Web API it first calls the Values Controller (discussed in the next section) constructor before the specific request method (ex: Get). In the controller constructor, it checks to see if the cache object (pointing to the default instance) has a specific key-value pair: a key of “People” and a value (reference object) of List<string> representing a list of characters from Star Trek TNG. If the cache does not contain the key “People” it will first create a default List<string> of characters from Star Trek TNG and then set the cache with the key-value pair.
Next the specific Web API request method is called (GET, POST, PUT, or DELETE) in the Values controller. The CRUD actions are done on the cache object by reference. This means that the CRUD operates do not directly modify (indexer) or set (cache.Set) the key-value cache entry. This allows the original key-value entry in the cache to retain its original policy and expiration settings (see notes above). Instead, each CRUD application copies a reference to the key-value entry in the cache (a List<string> is a reference type) and does the CRUD operations on the object reference. Thus, there is no need to utilize the indexer or cache.Set methods of adding/updating the cache key-value entry directly. As previously mentioned, the reason why we do not want to modify the cache key-value entry directly (either with indexer or cache.Set) is because we would be overwriting the original policy and expiration that was set when it was originally created.
Values Controller
This values controller is the generic default Web API controller implementation and I did not rename or do any fancy routing other than use the default routing. This is a basic demo with focus on showing the Memory Cache and client implementations. However, I did modify and customize the GET, POST, PUT, and DELETE methods to simulate CRUD operations that could be done on a backend database. The Memory Cache simulates “conserving” expensive resource calls to the backend database by keeping the contents in memory for a specified duration. The code is available on GitHub [here].
Constructor
The Values constructor gets called each time the client does a Web API request. A controller instance is created after the construction for the Web API actions. The constructor provides an opportunity to check the existence of the key-value pair in the MemoryCache and if it does not exist, call a backend database to repopulate it. In this demo, I am simulating the database call by reinitializing a default List of string objects called “People” and then placing the key-value item in the MemoryCache object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/// <summary> /// ValuesController /// Constructs a temporary collection for the /// demo. The collection is a list of string /// entities representing characters from /// Star Trek TNG /// </summary> public ValuesController() { if (!cache.Contains("People")) { // Simple List of People for CRUD Example people = new List<string>(); // Add some generic values people.Add("Patrict Stewart"); people.Add("Brent Spiner"); people.Add("Jonathon Frakes"); people.Add("Marina Sirtus"); people.Add("Gates McFadden"); people.Add("Michael Dorn"); people.Add("LeVar Burton"); people.Add("Wil Wheaton"); people.Add("Denise Crosby"); people.Add("Majel Barrett"); people.Add("Colm Meaney"); people.Add("Whoopi Goldberg"); people.Add("John Di Lancie"); people.Add("Diana Muldaur"); people.Add($"Cached: {DateTime.Now.ToLongTimeString()}"); // Cache Expiration set to 2 minutes in the future CacheItemPolicy policy = new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(2) }; // Add a new Cache! cache.Add("People", people, policy); } // end of if } // end of constructor |
Get (Collection)
The Get (Collection) retrieves the key-value entry from the cache and then returns an IEnumerable collection to the caller.
1 2 3 4 5 6 7 8 9 10 11 |
/// <summary> /// Get (Collection) /// Gets the entire collection of the entities /// GET api/values /// </summary> /// <returns>a collection (IEnumerable) of entities </returns> public IEnumerable<string> Get() { // Get the List of Entities from the Cache return (List<string>)cache.Get("People"); } |
Get (Entity Id)
The Get (Single Entity) first retrieves the key-value entry from the cache as an IEnumerable collection, then returns the object value at that index location in the collection. It first checks to see if the caller’s index is in range before returning the entity. If the specified index is out of range, it will return a BadRequest response code to the caller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/// <summary> /// Get (Single) /// Gets the entity by the identifier /// GET api/values/5 /// IF index out of range => Returns a HttpStatusCode.BadRequest /// </summary> /// <param name="id">identifier (int) of the entity</param> /// <returns>the entity value (string) at the identifer</returns> public string Get(int id) { // Get the List of Entities from the Cache people = (List<string>)cache.Get("People"); // Don't Process if ID is out of Range 0-entity,count if (id >= people.Count || id < 0) { // Make a bad response and throw it HttpResponseMessage message = new HttpResponseMessage(HttpStatusCode.BadRequest); throw new HttpResponseException(message); } else { return people[id]; } } // end of method |
Post
The method first retrieves the cache value (List object) from a key “People” into a List object in the method. Because the object reference is set to the local object, all actions done on the local List<string> object takes effect in the key-value entry in the cache. Post simply adds a new entry to the List<string> object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/// <summary> /// Post /// Adds a new entity to the collection /// POST api/values /// </summary> /// <param name="value">the value (string) of the entity</param> public void Post([FromBody]string value) { // Get the List of Entities from the Cache people = (List<string>)cache.Get("People"); // Add the entity people.Add(value); } // end of method |
Put
The method first retrieves the cache value (List object) from a key “People” into a List object in the method. Because the object reference is set to the local object, all actions done on the local List<string> object takes effect in the key-value entry in the cache. Put simply replaces an entry to the List<string> object at the specified index. However, the method first checks to determine if the index is in range. If out of range, it will return a BadRequest response code to the caller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/// <summary> /// Put /// Replaces the entity with an identifier with a new value. /// PUT api/values/5 /// IF index out of range => Returns a HttpStatusCode.BadRequest /// </summary> /// <param name="id">identifier (int) of the entity to replace</param> /// <param name="value">value (string) to replace the existing entity value</param> public void Put(int id, [FromBody]string value) { // Get the List of Entities from the Cache people = (List<string>)cache.Get("People"); // Don't Process if ID is out of Range 0-entity,count if (id >= people.Count || id < 0) { // Make a bad response and throw it HttpResponseMessage message = new HttpResponseMessage(HttpStatusCode.BadRequest); throw new HttpResponseException(message); } //Update the Entity people[id] = value; } // end of method |
Delete
The method first retrieves the cache value (List object) from a key “People” into a List object in the method. Because the object reference is set to the local object, all actions done on the local List<string> object takes effect in the key-value entry in the cache. Delete simply deletes an entry in the List<string> object at the specified index. However, the method first checks to determine if the index is in range. If out of range, it will return a BadRequest response code to the caller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/// <summary> /// Delete /// Deletes an entity based on the id /// DELETE api/values/5 /// IF index out of range => Returns a HttpStatusCode.BadRequest /// </summary> /// <param name="id">identifier (id) of the entity</param> public void Delete(int id) { // Get the List of Entities from the Cache people = (List<string>)cache.Get("People"); // Don't Process if ID is out of Range 0-entity,count if (id >= people.Count || id < 0) { // Make a bad response and throw it HttpResponseMessage message = new HttpResponseMessage(HttpStatusCode.BadRequest); throw new HttpResponseException(message); } // Delete the Entity people.RemoveAt(id); } // end of method |
Client “Tester to Web API Service” Windows Form Application
The client “tester to service” is a simple windows form application project (Client) in the same solution that connects to the Web API by constructing a .Net HttpClient object and setting the path to the Web API service. The client program has a useful GUI to aid the user to test the GET, POST, PUT, and DELETE calls to the Web API with parameters specified in the data entry boxes. The project code is available on GitHub [here].
Features
The client application was meant to only quickly demonstrate consumption of the Web API service, so this client has limited scope of features. Some of the client application features are:
Simple Design
The user interface is simplified to do basic demo, testing, verification, and showing results in the GUI. After executing a Get(*) or any request other than Get(1), the user can select an item in the list and the index data entry box will populate with the correct entity index. The user can also manually enter an index, but the list value will not be selected. The user can select an item from the list, let the index box auto-update, and then perform PUT, DELETE, or GET(1) Web API requests. Note: The GET(*) is called after every PUT, POST, and DELETE request to update and refresh the contents of the local list shown in the GUI. The user can also easily POST a new entity by entering a string value into the data entry box and clicking on POST. The user can also select an existing item in the list, the index will show, and then type an entry into the Value text box to PUT or replace an entry. Likewise, the user can select an entry in the list box, the index selection will update, and then DELETE the entity.
Results in the GUI
After each Web API call (except Get (1)), the GUI List Box is populated with the current contents of the controller’s cache key-value entry. This would simulate a database query on the service and seeing the results of the query in the client GUI. For the Get(1) request, the List Box is only populated with the entity at the index that was requested by the user. Note: The GET(*) is called after every PUT, POST, DELETE request to update and refresh the contents of the local list shown in the GUI.
Feedback of Bad Web API Responses
If the service returns any response other than “successful” the problem is caught in the client and alerts the user with a Message Box dialog with the code and error.
MemoryCache Verification
The service places a string object indicating the current DateTime that the MemoryCache key-value entry was created. This information is returned to the client in the same List<string> object with the remainder of the “People” items. This allows the user to verify that the MemoryCache entry is expiring on schedule and refreshing its contents with the default values per the design. The user can verify the MemoryCache is working in the Client by selecting the Get(*) button after the policy expiration interval to verify the DateTime is changed.
Program Code
The code behind file is for the client tester and manages the application in one file. The code is available on GitHub [here].
Fields
Private fields allow the client to work with the following:
- Http Client for Web Requests
- Path to Web API Service
Methods
Main
This main method is the entry point of the client tester application. It sets up the HttpClient object with the Web API Values Controller address path.
Events
There are multiple button click events to handle the user requests. Some events may first validate user text input from the GUI, then call a helper method to further assemble the HttpClient object for the GET, POST, PUT, or DELETE requests.
Helper Methods for Web API
These methods run after a button click request, work to further setup the HttpClient object, communicate with the Web API service, and process the results asynchronously. If there is a response other than successful, it throws an exception that is handled further upstream with a user dialog alerting the user of the Web API issue during testing.
Demo
A video demonstration of the project is available on YouTube here:
Screen captures are shown below for various user activities:
- After all projects are loaded from Visual Studio
- After Get(*)
- After Get(1) with the index specified
- After Post with the Value specified
- After Put with the Index and Value specified.
- After DELETE with the Index specified
- Cache Expiration
Code
The entire project code repository is available on GitHub [here].