How to consume multiple REST API in C# - c#

Initially, I have consumed a single REST API request in C# and desearilsed the JSON response. I want to consume multiple API(2 or 3). How do I modify my code for that?
static void Main(string[] args)
{
api1();
}
public static void api1()
{
var client = new RestClient("https://dummy.restapiexample.com/api/");
var request = new RestRequest("Data");
var response = client.Execute(request);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
string rawResponse = response.Content;
var root = JsonConvert.DeserializeObject<Rootobject>(rawResponse)
}
}
I tried to create function for each API request but I am not sure what will be the return type and how am i going to call all the functions.
public async Task<Var> api2()
{
var client = new RestClient("https://dummy.restapiexample2.com/api2/");
var request = new RestRequest("Data");
var response = client.Execute(request);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
string rawResponse = response.Content;
var root = JsonConvert.DeserializeObject<Rootobject>(rawResponse)
return root;
}
else
return null;
}
static void Main(string[] args)
{
api1();
api2();
}

You can't return var as you have done in above, you need to return what are expecting to get from that method, for example, Rootobject.
public async Task Main(string[] args)
{
var result = await Api2();
var myProVal = result.MyProperty;
}
public static async Task <Rootobject> Api2()
{
var client = new RestClient("https://dummy.restapiexample2.com/api2/");
var request = new RestRequest("Data");
var response = await client.ExecuteAsync(request);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
string rawResponse = response.Content;
var root = JsonConvert.DeserializeObject<Rootobject>(rawResponse);
return root;
}
else
return null;
}
}
And here you have been using async. The async keyword turns a method into an async method, which allows you to use the await keyword in its body. When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method. if you don't have such a requirement don't use async, just make the method as synchronous.
Read more: https://learn.microsoft.com/en-us/dotnet/csharp/async
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-7.0
Updated:
in the above code snippet, my Rootobject class as follows,
public class Rootobject
{
public int MyProperty { get; set; }
public string Name { get; set; }
public string age { get; set; }
public string addr { get; set; }
}
In the Main method, MyProperty is a sample property in that class. In your case it should be another one, it's up to you. I think it's better if you can learn the basics before moving to this level. good luck

Related

An object reference is required for the non-static field, method, or property 'Program.c8y_Event_MSG' [deserializeJSON]

I have my main method static async Task Main(string[] args) and another async method public static async Task c8y_pushEvent(string Event_MSG) inside my class Program. I'm trying to call the public static async Task c8y_pushEvent(string Event_MSG) from inside static async Task Main(string[] args).
class Program
{
int int_lastPushedId = 235;
int get_maxID_old = 250;
string c8y_Event_MSG = String.Empty;
static async Task Main(string[] args)
{
//Here's where I am getting the problem.
for(int event_dbID_lp = int_lastPushedId + 1 ; event_dbID_lp <= get_maxID_old ; event_dbID_lp++)
{
SqlCommand command4 = new SqlCommand(queryString4, connection);
command4.Parameters.Add("#ID", (int)event_dbID_lp);
SqlDataReader reader4 = command4.ExecuteReader();
while (reader4.Read())
{
c8y_Event_MSG = Convert.ToString(reader4[0]);
await c8y_pushEvent(c8y_Event_MSG);
}
reader4.Close();
}
}
public static async Task c8y_pushEvent(string Event_MSG)
{
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
var services = serviceCollection.BuildServiceProvider();
var httpClientFactory = services.GetRequiredService<IHttpClientFactory>();
List<KeyValue> keyvalue = JsonConvert.DeserializeObject<List<KeyValue>>(Event_MSG);
string controllerName = keyvalue[0].ToString();
JArray EventMSG = JArray.Parse(Event_MSG);
var item = EventMSG[0];
var httpClientgetControllerId = httpClientFactory.CreateClient("getControllerId");
var httpClientcreateGolfControllerEvent = httpClientFactory.CreateClient("createGolfControllerEvent");
var request1 = await httpClientgetControllerId.GetAsync($"abc/xyz/{controllerName}");
if (request1.IsSuccessStatusCode)
{
HttpResponseMessage request2 = await httpClientcreateGolfControllerEvent.PostAsync("event/events", stringContent);
//stringContent is fetched from some other lines which I've not
//included here
}
}
private static void ConfigureServices(ServiceCollection services)
{
services.AddHttpClient("getControllerId", options =>
{
options.BaseAddress = new Uri("https://myurl.com/");
options.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic","auth_value");
});
services.AddHttpClient("createGolfControllerEvent", options =>
{
options.BaseAddress = new Uri("https://myurl.com/");
options.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic","auth_value");
options.DefaultRequestHeaders.Add("Accept", "application/vnd.com.nsn.xyz.event+json");
});
}
public class KeyValue
{
[JsonProperty("ControllerName")]
public string ControllerName { get; set; }
public override string ToString()
{
return String.Format(ControllerName.ToString());
}
}
}
I'm getting the error
An object reference is required for the non-static field, method, or property 'Program.c8y_Event_MSG' [deserializeJSON]
on line
await c8y_pushEvent(c8y_Event_MSG);
How can I fix this issue?
Additional question:
The difference between int_lastPushedId and get_maxID_old can be as big as 1000+
hence there can be 1000+ iterations.
Is my implementation of IHttpClientFactory suitable for this, making sure there's no exhaustion of socket/ports?
Main error
It looks like c8y_Event_MSG should be local:
while (reader4.Read())
{
var c8y_Event_MSG = Convert.ToString(reader4[0]);
await c8y_pushEvent(c8y_Event_MSG);
}
HttpClient qestion
All of the Dependency Injection code seem unnecessary. I cannot think of a situation where var serviceCollection = new ServiceCollection(); in a for loop
would be a good idea.
Here's a simpler version that should do the job:
class Program
{
static HttpClient httpClient;
static async Task Main(string[] args)
{
httpClient = new HttpClient();
//Setup auth and base address
for(...)
{
...
c8y_pushEvent(...)
}
}
public static async Task c8y_pushEvent(string Event_MSG, HttpClient)
{
List<KeyValue> keyvalue = JsonConvert.DeserializeObject<List<KeyValue>>(Event_MSG);
string controllerName = keyvalue[0].ToString();
JArray EventMSG = JArray.Parse(Event_MSG);
var item = EventMSG[0];
var request1 = await httpClient.GetAsync($"abc/xyz/{controllerName}");
}

How to properly make app async with "async" and "await"?

class Website
{
public Website(string link)
{
_linkToWeb = new RestClient(link);
}
public async Task<string> DownloadAsync(string path)
{
var request = new RestRequest(path, Method.GET);
var response = _linkToWeb.ExecuteAsync(request);
return response.Result.Content;
}
public RestClient _linkToWeb { get; set; }
}
class Program
{
public static Website API = new Website("https://api.collegefootballdata.com");
public static async Task<string> _downloadTeamsFromAPI()
{
return API.Download("/teams/fbs");
}
public static async Task<string> _downloadAdvancedInfoFromAPI()
{
return API.Download("/stats/season/advanced?year=2010");
}
public static async Task<Teams> _addTeamToDB(Teams item)
{
var tmp = new Teams
{
School = item.School,
Abbreviation = item.Abbreviation,
Conference = item.Conference,
Divison = item.Divison,
Color = item.Color,
Alt_Color = item.Alt_Color,
Team = await _getAdvancedInfoFromAPI(item.Conference)
};
return tmp;
}
public static async Task<string> _getAdvancedInfoFromAPI(string _conferenceName)
{
List<Advanced> advancedDataList = new List<Advanced>();
var advancedData = await _downloadAdvancedInfoFromAPI();
var advancedDataDeserialized = JsonSerializer.Deserialize<Advanced[]>(advancedData, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
foreach (var item in advancedDataDeserialized)
{
advancedDataList.Add(new Advanced
{
Team = item.Team,
//Conference = item.Conference,
Year = item.Year,
excludeGarbageTime = item.excludeGarbageTime,
startWeek = item.startWeek,
endWeek = item.endWeek
});
}
return await _lookForMatch(_conferenceName, advancedDataList);
}
public static async Task<string> _lookForMatch(string _Confa, List<Advanced> lista)
{
return lista
.Where(x => x.Conference == _Confa)
.Select(x => x.Team)
.FirstOrDefault();
}
public static async Task Main()
{
Console.WriteLine("Odpaliłem program!\n");
using var db = new Context();
db.Database.EnsureCreated();
Console.WriteLine("Stworzylem baze!\n");
var teams = await _downloadTeamsFromAPI();
var deserializer = JsonSerializer.Deserialize<Teams[]>(teams, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
Console.WriteLine("Zdeserializowalem dane!\n");
foreach (var item in deserializer)
{
db.Teams.Add(await _addTeamToDB(item));
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss"));
Console.WriteLine("Dodalem element do bazy...\n");
};
db.SaveChanges();
Console.WriteLine("Zapisalem dane do bazy!");
}
}
I know it's a noob question but I don't know how to make it work :/
I want to make it a bit asynchronous, because the words async and await doesn't exactly make it more asynchronous, but I don't know how to make it work anyhow asynchronous.
The app first downloads the information from API, deserializes it and stores it into var type variable. Then it downloads the advanced info from API and joins it by "Conference" item. (that is on purpose, even though it's not optimal).
There are a lot of asyncs and awaits but I don't think it anyhow runs asynchronously. What should I change to make this app actually async?
I appreciate your motive to write asynchronous code to make your application more scalable.
However after going through the sample code posted, I am afraid you need to do more learning on the concepts of asynchronous programming rather than just jumping into the console and trying to write some code which looks like asynchronous code.
Start slowly and try to understand the purpose of Task library, when to use it. What await does behind the scenes. When to wrap a return type with Task and when to mark a method as async. These are some of the main keywords which you come across in asynchronous code and a good understanding of these is a must to write/understand asynchronous code.
There are plenty of resources available online to get a hang of these concepts. For starters, you can begin looking into Microsoft Documentation
Having said that, inline is a rewrite of the sample code with proper use of async/await.
Please use this for references purpose only. Do not try to put into some production code until unless you have a good understanding of the concept.
Necessary comments are provided to explain some critical changes made.
class Website
{
public Website(string link)
{
_linkToWeb = new RestClient(link);
}
public async Task<string> DownloadAsync(string path)
{
var request = new RestRequest(path, Method.GET);
var response = await _linkToWeb.ExecuteAsync(request); //await an asynchronous call.
return response.Content; //No need to call response.Result. response content can be fetched after successful completion of asynchronous call.
}
public RestClient _linkToWeb { get; set; }
}
class Program
{
public static Website API = new Website("https://api.collegefootballdata.com");
public static async Task<string> _downloadTeamsFromAPI()
{
return await API.DownloadAsync("/teams/fbs");
}
public static async Task<string> _downloadAdvancedInfoFromAPI()
{
return await API.DownloadAsync("/stats/season/advanced?year=2010");
}
public static async Task<Teams> _addTeamToDB(Teams item)
{
var tmp = new Teams
{
School = item.School,
Abbreviation = item.Abbreviation,
Conference = item.Conference,
Divison = item.Divison,
Color = item.Color,
Alt_Color = item.Alt_Color,
Team = await _getAdvancedInfoFromAPI(item.Conference)
};
return tmp;
}
//Return type has to be Task<Teams> rather than Task<string> because the return object is Teams.
public static async Task<Teams> _getAdvancedInfoFromAPI(string _conferenceName)
{
List<Advanced> advancedDataList = new List<Advanced>();
var advancedData = await _downloadAdvancedInfoFromAPI();
var advancedDataDeserialized = JsonSerializer.Deserialize<Advanced[]>(advancedData, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
foreach (var item in advancedDataDeserialized)
{
advancedDataList.Add(new Advanced
{
Team = item.Team,
//Conference = item.Conference,
Year = item.Year,
excludeGarbageTime = item.excludeGarbageTime,
startWeek = item.startWeek,
endWeek = item.endWeek
});
}
return _lookForMatch(_conferenceName, advancedDataList);
}
//Return type is Teams and not string.
//Moreover Task<string> not required because we are not awaiting method call in this method.
public static Teams _lookForMatch(string _Confa, List<Advanced> lista)
{
return lista.Where(x => x.Conference == _Confa)
.Select(x => x.Team)
.FirstOrDefault();
}
public static async Task Main()
{
Console.WriteLine("Odpaliłem program!\n");
using var db = new Context();
db.Database.EnsureCreated();
Console.WriteLine("Stworzylem baze!\n");
var teams = await _downloadTeamsFromAPI();
var deserializer = JsonSerializer.Deserialize<Teams[]>(teams, new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
Console.WriteLine("Zdeserializowalem dane!\n");
foreach (var item in deserializer)
{
db.Teams.Add(await _addTeamToDB(item));
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss"));
Console.WriteLine("Dodalem element do bazy...\n");
};
db.SaveChanges();
Console.WriteLine("Zapisalem dane do bazy!");
}
}

C# Using async await inside foreach to execute API

Is there a way to use a foreach() to call an API multiple times with different parameters. Currently the code only executes the first item (itemOne) and not the rest of the items. I want to get back all the results and then exit the function.
The method that calls TestExecute() comes from the controller. I haven't shown controller method since it only calls TestLogs. There are no errors being shown. Basically I want to loop through all three of the items to get the itemId and store it in the list. In this case it would call the API 3 times and store the results and only then do I want to exit this function.
public class TestLogs
{
private readonly ITest _test;
public TestLogs()
{
_test = new test();
}
private async Task<TestProjectsDto> GetProjectId()
{
var date = new DateTime(2020, 04, 15);
var sapProjectNum = new TestProjectsDto
{
Projects = new List<TestProjectDto>()
};
var list = new List<string>()
{
"itemOne",
"itemTwo",
"itemThree"
};
foreach (var divList in list)
{
var proIds = await _test.GetProjectItem(divList, date);
if (proIds != null)
{
sapProjectNum.Projects.AddRange(proIds.Projects);
}
}
return sapProjectNum;
}
public async Task TestExecute()
{
// Where I call GetProjectId();
var listProjectIds = GetProjectId();
// etc
}
}
}
public class TestService : ITest
{
//etc
public async Task<TestProjectsDto> GetProjectOnSpecificDate(string divisionName, DateTime date)
{
var url = $"{test}/GetProjectOnSpecificDate.xsjs?TYPE=item={item}&Date={date:yyyy-MM-dd}";
var response = await HttpClient.GetAsync(url).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsAsync<TestProjectsDto>().ConfigureAwait(false);
return content;
}
return null;
}
}

Moq unit testing access local variable

I am trying to create an INTEGRATION Test using Moq for some code that involves a REST request.
In regular usage, the code would go out and create a Report record and have other impacts in a 3rd party system.
With the Moq test, the Execute call from the RestSharp IRestClient can be substituted for a dummy method that does nothing. For a successful INTEGRATION test, there are 2 requirements: (a) REQUEST xml looks correct (b) RESPONSE json is returned. I'd like to be able to execute most of the code involved in the integration and inspect a local variable from the system under test in the xUnit code assertions. However, I can't seem to access the local variable using Moq, unless I add some code artifacts around testing.
I have created two projects to illustrate. Hope you can point me in the right direction. Perhaps the code needs to be restructured or a new Mock object for the CommandHandler needs to be created?
Thanks!
TEST PROJECT
using Mocking; // System Under Test
using Moq;
using Newtonsoft.Json.Linq;
using RestSharp;
using System.Net;
using System.Threading;
using Xunit;
namespace MockingTest
{
public class UnitTest1
{
[Fact]
public async void SubmitReport_WithPerson_CanProcessSubmitSuccessfully()
{
// ----------------------------------------------------------------------------------
// Arrange
// ----------------------------------------------------------------------------------
Person person = new Person();
person.Name = "Test";
string testRequestXML = GetTestRequestXML(person);
string testResponseXML = "OK";
// Construct the Mock Rest Client. This should allow most of the submission process to be run -
// but the actual Execute to call CMS will not be done - instead the Mock framework will return
// an arbitrary response as defined below.
var mockRestClient = new Mock<IRestClient>();
RestResponse testRestResponse = GetTestRestResponse(System.Net.HttpStatusCode.OK, string.Empty, ResponseStatus.Completed, testResponseXML);
mockRestClient.Setup(rc => rc.Execute(It.IsAny<IRestRequest>()))
.Returns(testRestResponse);
// ----------------------------------------------------------------------------------
// Act
// ----------------------------------------------------------------------------------
Command command = new Command(person);
CancellationToken cancellationToken = new CancellationToken();
CommandHandler commandHandler = new CommandHandler(mockRestClient.Object); // CommandHandler is the "System Under Test"
string result = await commandHandler.Handle(command, cancellationToken);
JToken responseToken = JToken.Parse(result);
string responseXML = responseToken.SelectToken("response").ToString();
string requestXML = responseToken.SelectToken("request").ToString(); // Normally this would not be available.
// ----------------------------------------------------------------------------------
// Assert
// ----------------------------------------------------------------------------------
Assert.Equal(requestXML, testRequestXML); // Handed back in JSON - normally this would not be the case.
Assert.Equal(commandHandler.ReportXMLRequest, testRequestXML); // Handed back in Property - normally this would not be the case.
}
private RestResponse GetTestRestResponse(HttpStatusCode httpStatusCode, string httpErrorMessage, ResponseStatus httpResponseStatus, string responseXML)
{
RestResponse testRestResponse = new RestResponse();
testRestResponse.StatusCode = httpStatusCode;
testRestResponse.ErrorMessage = httpErrorMessage;
testRestResponse.ResponseStatus = httpResponseStatus;
testRestResponse.Content = responseXML;
return testRestResponse;
}
private string GetTestRequestXML(Person person)
{
// Sample XML.
string xml = string.Empty;
xml = xml + "<xml>";
xml = xml + "<report>";
xml = xml + "<status>" + "Initialized" + "</status>";
xml = xml + "<person>" + person.Name + "</person>";
xml = xml + "</report>";
return xml;
}
}
}
SYSTEM UNDER TEST
using Newtonsoft.Json.Linq;
using RestSharp;
using System;
using System.Threading;
using System.Threading.Tasks;
// System Under Test
namespace Mocking
{
public class Person
{
public string Name { get; set; }
}
public class ReportStatus
{
public string Status { get; private set; }
public ReportStatus ()
{
this.Status = "Initialized";
}
}
public class Report
{
public Person Person { get; private set; }
public ReportStatus ReportStatus { get; private set; }
public Report (Person person)
{
Person = person;
ReportStatus = new ReportStatus();
}
}
public class Command
{
public Person Person { get; private set; }
public Command (Person person)
{
this.Person = person;
}
}
public class CommandHandler
{
public string ReportXMLRequest { get; private set; } // Property to permit validation.
private readonly IRestClient RestClient;
//// Using DI to inject infrastructure persistence Repositories - this is the normal call.
//public CommandHandler(IMediator mediator, IReportRepository reportRepository, IIdentityService identityService)
//{
// ReportXMLRequest = string.Empty;
// RestClient = new RestClient();
//}
// MOQ Addition - Overload constructor for Moq Testing.
public CommandHandler(IRestClient restClient)
{
ReportXMLRequest = string.Empty;
RestClient = restClient;
}
public async Task<string> Handle(Command command, CancellationToken cancellationToken)
{
Report report = new Report(command.Person);
string reportResult = Submit(report);
return reportResult;
}
private string Submit(Report report)
{
string responseXML = string.Empty;
string localVariableForRequestXML = GetRequestXML(report);
// MOQ Addition - Set Property to be able to inspect it from the integration test.
this.ReportXMLRequest = localVariableForRequestXML;
IRestClient client = RestClient;
string baseType = client.GetType().BaseType.FullName;
client.BaseUrl = new Uri("http://SampleRestURI");
RestRequest request = new RestRequest(Method.POST);
request.AddParameter("application/xml", localVariableForRequestXML, ParameterType.RequestBody);
// Normally, this REST request would go out and create a Report record and have other impacts in a 3rd party system.
// With Moq, the Execute call from the RestSharp IRestClient can be substituted for a dummy method.
// For a successful INTEGRATION test, there are 2 requirements:
// (a) REQUEST xml looks correct (b) RESPONSE json is returned.
**IRestResponse response = client.Execute(request);**
responseXML = response.Content;
// MOQ Addition - Do something... e.g. return JSON response with extra information.
JObject json = null;
if (baseType.ToLowerInvariant().Contains("moq"))
{
json = new JObject(
new JProperty("response", responseXML),
new JProperty("request", localVariableForRequestXML)
);
}
else
{
json = new JObject(new JProperty("response", responseXML));
}
string jsonResponse = json.ToString();
return jsonResponse;
}
private string GetRequestXML(Report report)
{
// Sample XML - normally this would be quite complex based on Person and other objects.
string xml = string.Empty;
xml = xml + "<xml>";
xml = xml + "<report>";
xml = xml + "<status>" + report.ReportStatus.Status + "</status>";
xml = xml + "<person>" + report.Person.Name + "</person>";
xml = xml + "</report>";
return xml;
}
}
}
Apart from the poorly designed subject and test, (which appears to be more of a unit test than an integration test), the mocked dependency can be used to retrieve the provided input.
You can either use a Callback
//...code removed for brevity
string requestXML = string.Empty;
mockRestClient
.Setup(_ => _.Execute(It.IsAny<IRestRequest>()))
.Callback((IRestRequest request) => {
var parameter = request.Parameters.Where(p => p.Name == "application/xml").FirstOrDefault();
if(parameter != null && parameter.Value != null) {
requestXML = parameter.Value.ToString();
}
})
.Returns(testRestResponse);
//...code removed for brevity
Assert.Equal(requestXML, testRequestXML);
Or do the same directly in the Returns delegate
//...code removed for brevity
string requestXML = string.Empty;
mockRestClient
.Setup(_ => _.Execute(It.IsAny<IRestRequest>()))
.Returns((IRestRequest request) => {
var parameter = request.Parameters.Where(p => p.Name == "application/xml").FirstOrDefault();
if(parameter != null && parameter.Value != null) {
requestXML = parameter.Value.ToString();
}
return testRestResponse;
});
//...code removed for brevity
Assert.Equal(requestXML, testRequestXML);
There is no need to modify the subject under test specifically for the purposes of testing. The injected abstraction should be enough to provided access to desired variables via the mock.
In the commented out constructor of the subject
RestClient = new RestClient(); /<-- don't do this
should not be done as it tightly couples the class to the rest client. There is also no need for the overload. Move the abstraction to the initial constructor. It is already accepting abstractions.
// Using DI to inject infrastructure persistence Repositories - this is the normal call.
public CommandHandler(IMediator mediator, IReportRepository reportRepository,
IIdentityService identityService, IRestClient restClient) {
RestClient = restClient;
//...assign other local variables
}
If the test is meant to be async then have it return a Task and not async void
public async Task SubmitReport_WithPerson_CanProcessSubmitSuccessfully() {
//...
}
But given that the subject appears incomplete, it is not certain that it is actually using async flow as the following method
public async Task<string> Handle(Command command, CancellationToken cancellationToken)
{
Report report = new Report(command.Person);
string reportResult = Submit(report);
return reportResult;
}
contains no awaited methods.

How to use ETag in Web API using action filter along with HttpResponseMessage

I have a ASP.Net Web API controller which simply returns the list of users.
public sealed class UserController : ApiController
{
[EnableTag]
public HttpResponseMessage Get()
{
var userList= this.RetrieveUserList(); // This will return list of users
this.responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ObjectContent<List<UserViewModel>>(userList, new JsonMediaTypeFormatter())
};
return this.responseMessage;
}
}
and an action filter attribute class EnableTag which is responsible to manage ETag and cache:
public class EnableTag : System.Web.Http.Filters.ActionFilterAttribute
{
private static ConcurrentDictionary<string, EntityTagHeaderValue> etags = new ConcurrentDictionary<string, EntityTagHeaderValue>();
public override void OnActionExecuting(HttpActionContext context)
{
if (context != null)
{
var request = context.Request;
if (request.Method == HttpMethod.Get)
{
var key = GetKey(request);
ICollection<EntityTagHeaderValue> etagsFromClient = request.Headers.IfNoneMatch;
if (etagsFromClient.Count > 0)
{
EntityTagHeaderValue etag = null;
if (etags.TryGetValue(key, out etag) && etagsFromClient.Any(t => t.Tag == etag.Tag))
{
context.Response = new HttpResponseMessage(HttpStatusCode.NotModified);
SetCacheControl(context.Response);
}
}
}
}
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var request = context.Request;
var key = GetKey(request);
EntityTagHeaderValue etag;
if (!etags.TryGetValue(key, out etag) || request.Method == HttpMethod.Put || request.Method == HttpMethod.Post)
{
etag = new EntityTagHeaderValue("\"" + Guid.NewGuid().ToString() + "\"");
etags.AddOrUpdate(key, etag, (k, val) => etag);
}
context.Response.Headers.ETag = etag;
SetCacheControl(context.Response);
}
private static void SetCacheControl(HttpResponseMessage response)
{
response.Headers.CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(60),
MustRevalidate = true,
Private = true
};
}
private static string GetKey(HttpRequestMessage request)
{
return request.RequestUri.ToString();
}
}
The above code create an attribute class to manage ETag. So on the first request, it will create a new E-Tag and for the subsequent request it will check whether any ETag is existed. If so, it will generate Not Modified HTTP Status and return back to client.
My problem is, I want to create a new ETag if there are changes in my user list, ex. a new user is added, or an existing user is deleted. and append it with the response. This can be tracked by the userList variable.
Currently, the ETag received from client and server are same from every second request, so in this case it will always generate Not Modified status, while I want it when actually nothing changed.
Can anyone guide me in this direction?
My requirement was to cache my web api JSON responses... And all the solutions provided don't have an easy "link" to where the data is generated - ie in the Controller...
So my solution was to create a wrapper "CacheableJsonResult" which generated a Response, and then added the ETag to the header. This allows a etag to be passed in when the controller method is generated and wants to return the content...
public class CacheableJsonResult<T> : JsonResult<T>
{
private readonly string _eTag;
private const int MaxAge = 10; //10 seconds between requests so it doesn't even check the eTag!
public CacheableJsonResult(T content, JsonSerializerSettings serializerSettings, Encoding encoding, HttpRequestMessage request, string eTag)
:base(content, serializerSettings, encoding, request)
{
_eTag = eTag;
}
public override Task<HttpResponseMessage> ExecuteAsync(System.Threading.CancellationToken cancellationToken)
{
Task<HttpResponseMessage> response = base.ExecuteAsync(cancellationToken);
return response.ContinueWith<HttpResponseMessage>((prior) =>
{
HttpResponseMessage message = prior.Result;
message.Headers.ETag = new EntityTagHeaderValue(String.Format("\"{0}\"", _eTag));
message.Headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromSeconds(MaxAge)
};
return message;
}, cancellationToken);
}
}
And then, in your controller - return this object:
[HttpGet]
[Route("results/{runId}")]
public async Task<IHttpActionResult> GetRunResults(int runId)
{
//Is the current cache key in our cache?
//Yes - return 304
//No - get data - and update CacheKeys
string tag = GetETag(Request);
string cacheTag = GetCacheTag("GetRunResults"); //you need to implement this map - or use Redis if multiple web servers
if (tag == cacheTag )
return new StatusCodeResult(HttpStatusCode.NotModified, Request);
//Build data, and update Cache...
string newTag = "123"; //however you define this - I have a DB auto-inc ID on my messages
//Call our new CacheableJsonResult - and assign the new cache tag
return new CacheableJsonResult<WebsiteRunResults>(results, GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings, System.Text.UTF8Encoding.Default, Request, newTag);
}
}
private static string GetETag(HttpRequestMessage request)
{
IEnumerable<string> values = null;
if (request.Headers.TryGetValues("If-None-Match", out values))
return new EntityTagHeaderValue(values.FirstOrDefault()).Tag;
return null;
}
You need to define how granular to make your tags; my data is user-specific, so I include the UserId in the CacheKey (etag)
a good solution for ETag and in ASP.NET Web API is to use CacheCow . A good article is here.
It's easy to use and you don't have to create a custom Attribute.
Have fun
.u
I found CacheCow very bloated for what it does, if the only reason is, to lower the amount of data transfered, you might want to use something like this:
public class EntityTagContentHashAttribute : ActionFilterAttribute
{
private IEnumerable<string> _receivedEntityTags;
private readonly HttpMethod[] _supportedRequestMethods = {
HttpMethod.Get,
HttpMethod.Head
};
public override void OnActionExecuting(HttpActionContext context) {
if (!_supportedRequestMethods.Contains(context.Request.Method))
throw new HttpResponseException(context.Request.CreateErrorResponse(HttpStatusCode.PreconditionFailed,
"This request method is not supported in combination with ETag."));
var conditions = context.Request.Headers.IfNoneMatch;
if (conditions != null) {
_receivedEntityTags = conditions.Select(t => t.Tag.Trim('"'));
}
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var objectContent = context.Response.Content as ObjectContent;
if (objectContent == null) return;
var computedEntityTag = ComputeHash(objectContent.Value);
if (_receivedEntityTags.Contains(computedEntityTag))
{
context.Response.StatusCode = HttpStatusCode.NotModified;
context.Response.Content = null;
}
context.Response.Headers.ETag = new EntityTagHeaderValue("\"" + computedEntityTag + "\"", true);
}
private static string ComputeHash(object instance) {
var cryptoServiceProvider = new MD5CryptoServiceProvider();
var serializer = new DataContractSerializer(instance.GetType());
using (var memoryStream = new MemoryStream())
{
serializer.WriteObject(memoryStream, instance);
cryptoServiceProvider.ComputeHash(memoryStream.ToArray());
return String.Join("", cryptoServiceProvider.Hash.Select(c => c.ToString("x2")));
}
}
}
No need for setting up anything, set and forget. The way i like it. :)
I like the answer which was provided by #Viezevingertjes. It is the most elegant and "No need for setting up anything" approach is very convenient. I like it too :)
However I think it has a few drawbacks:
The whole OnActionExecuting() method and storing ETags in _receivedEntityTags is unnecessary because the Request is available inside the OnActionExecuted method as well.
Only works with ObjectContent response types.
Extra work load because of the serialization.
Also it was not part of the question and nobody mentioned it. But ETag should be used for Cache validation. Therefore it should be used with Cache-Control header so clients don't even have to call the server until the cache expires (it can be very short period of time depends on your resource). When the cache expired then client makes a request with ETag and validate it. For more details about caching see this article.
So that's why I decided to pimp it up a little but. Simplified filter no need for OnActionExecuting method, works with Any response types, no Serialization. And most importantly adds CacheControl header as well. It can be improved e.g. with Public cache enabled, etc... However I strongly advise you to understand caching and modify it carefully. If you use HTTPS and the endpoints are secured then this setup should be fine.
/// <summary>
/// Enables HTTP Response CacheControl management with ETag values.
/// </summary>
public class ClientCacheWithEtagAttribute : ActionFilterAttribute
{
private readonly TimeSpan _clientCache;
private readonly HttpMethod[] _supportedRequestMethods = {
HttpMethod.Get,
HttpMethod.Head
};
/// <summary>
/// Default constructor
/// </summary>
/// <param name="clientCacheInSeconds">Indicates for how long the client should cache the response. The value is in seconds</param>
public ClientCacheWithEtagAttribute(int clientCacheInSeconds)
{
_clientCache = TimeSpan.FromSeconds(clientCacheInSeconds);
}
public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
if (!_supportedRequestMethods.Contains(actionExecutedContext.Request.Method))
{
return;
}
if (actionExecutedContext.Response?.Content == null)
{
return;
}
var body = await actionExecutedContext.Response.Content.ReadAsStringAsync();
if (body == null)
{
return;
}
var computedEntityTag = GetETag(Encoding.UTF8.GetBytes(body));
if (actionExecutedContext.Request.Headers.IfNoneMatch.Any()
&& actionExecutedContext.Request.Headers.IfNoneMatch.First().Tag.Trim('"').Equals(computedEntityTag, StringComparison.InvariantCultureIgnoreCase))
{
actionExecutedContext.Response.StatusCode = HttpStatusCode.NotModified;
actionExecutedContext.Response.Content = null;
}
var cacheControlHeader = new CacheControlHeaderValue
{
Private = true,
MaxAge = _clientCache
};
actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{computedEntityTag}\"", false);
actionExecutedContext.Response.Headers.CacheControl = cacheControlHeader;
}
private static string GetETag(byte[] contentBytes)
{
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(contentBytes);
string hex = BitConverter.ToString(hash);
return hex.Replace("-", "");
}
}
}
Usage e.g: with 1 min client side caching:
[ClientCacheWithEtag(60)]
Seems to be a nice way to do it:
public class CacheControlAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public int MaxAge { get; set; }
public CacheControlAttribute()
{
MaxAge = 3600;
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
if (context.Response != null)
{
context.Response.Headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromSeconds(MaxAge)
};
context.Response.Headers.ETag = new EntityTagHeaderValue(string.Concat("\"", context.Response.Content.ReadAsStringAsync().Result.GetHashCode(), "\""),true);
}
base.OnActionExecuted(context);
}
}

Categories

Resources