Im using selenium chrome driver combined with cefsharp, the page im working on sends some kind of ping request(fetch/xhr) everytime an element is clicked the problem is that those request aren't initiated by selenium chrome driver so there is no wayy for me to track them and wait for the finish. Is there any way for selenium or js to track those requests?
With Selenium 4 you actually can intercept requests
Call Enable to start requests tracking
Call WaitForRequestResponse to wait for the specific request
If you can same requests you can clean already catch requests with Clear
public class WebDriverNetworkController
{
private const string RequestWillBeSent = "requestWillBeSent";
private const string ResponseReceived = "responseReceived";
private readonly IDriverProvider _driverProvider;
private readonly ILogger _log;
private readonly ConcurrentDictionary<DevToolsSession, List<HttpRequestModel>> _sentRequests = new();
public WebDriverNetworkController(IDriverProvider driverProvider, ILogger log)
{
_driverProvider = driverProvider;
_log = log;
}
private void HandleDevToolsEvent(object sender, DevToolsEventReceivedEventArgs e)
{
var session = sender as DevToolsSession;
switch (e.EventName)
{
case RequestWillBeSent:
{
OnRequestWillBeSent(session, e.EventData);
break;
}
case ResponseReceived:
{
OnResponseReceived(session, e.EventData);
break;
}
}
}
public void Enable()
{
_log.Information("Enabling network tracing");
var session = GetDevToolsSession();
session.DevToolsEventReceived += HandleDevToolsEvent;
var enableTask = session.Domains.Network.EnableNetwork();
enableTask.Wait();
}
public void Disable()
{
_log.Information("Disabling network tracing");
var session = GetDevToolsSession();
session.DevToolsEventReceived -= HandleDevToolsEvent;
var disableTask = session.Domains.Network.DisableNetwork();
disableTask.Wait();
}
public void Clear()
{
_log.Information("Clearing captured network tracing requests");
var sentRequests = GetRequests();
sentRequests.Clear();
}
public HttpRequestModel[] GetAllRequests() => GetRequests().ToArray();
public HttpRequestModel WaitForRequestResponse(HttpMethod method, string uri)
{
_log.Debug($"Waiting for {method.Method} request to {uri}");
var session = GetDevToolsSession();
HttpRequestModel request = null;
var waiter = new ConditionWaiter
{
Message = $"{method.Method} request to {uri}"
};
waiter.Until(() =>
{
request = GetLastSentRequest(method, uri, session);
return request?.Response != null;
});
return request;
}
private HttpRequestModel GetLastSentRequest(HttpMethod method, string uri, DevToolsSession session) =>
GetRequests(session).LastOrDefault(request => request.Method == method && request.Uri.EndsWith(uri));
private DevToolsSession GetDevToolsSession()
{
var driver = _driverProvider.GetDriver();
if (driver is IDevTools devTools)
{
return devTools.GetDevToolsSession();
}
throw new WebDriverException($"Could not cast {driver.GetType().Name} to {nameof(IDevTools)}");
}
private List<HttpRequestModel> GetRequests(DevToolsSession session = null) =>
_sentRequests.GetOrAdd(session ?? GetDevToolsSession(), _ => new());
private void OnRequestWillBeSent(DevToolsSession session, JToken eventData)
{
var requestModel = new HttpRequestModel
{
Id = eventData.SelectToken("requestId", true)!.Value<string>(),
Method = new(eventData.SelectToken("request.method", true)!.Value<string>()!),
Uri = eventData.SelectToken("request.url", true)!.Value<string>(),
Body = eventData.SelectToken("request")!.Value<string>("postData")
};
var sentRequests = _sentRequests.GetOrAdd(session, _ => new());
sentRequests.Add(requestModel);
}
private void OnResponseReceived(DevToolsSession session, JToken eventData)
{
var requestId = eventData.SelectToken("requestId", true)!.Value<string>();
var responseModel = new HttpResponseModel
{
Uri = eventData.SelectToken("response.url", true)!.Value<string>(),
StatusCode = (HttpStatusCode)eventData.SelectToken("response.status", true)!.Value<long>()
};
var sentRequests = _sentRequests[session];
var request = sentRequests.Last(request => request.Id == requestId);
request.Response = responseModel;
}
}
The code above is not ready to be used right away but should give a general understanding of the approach.
Related
I am trying to perform an action right after returning my response to a GraphQL API request.
Let me explain: I would like to develop a GraphQL server that receives a certain request from another server and sends a request to this other server just after answering the first request
In comparison, it looks like the OnActionExecuted function with ActionFilterAttribute but I can't implement it for GraphQl, maybe it works only for Rest APIs
I also found the function :
public async override Task AfterExecutionAsync(IExecutionContext context) from class GraphQL.Execution.DocumentExecutionListenerBase but I can't figure out how to implement it so that it triggers after returning my answer
this is the idea but of course it is not what i have tried haha
public async Task<sendLinkRequest> sendLinkStatusUpdate(newLinkStatusInput input)
{
return _sendLinkRequestService.sendLinkStatusUpdate(input);
//Doing this after my function return the result
var graphQLClient = new GraphQLHttpClient("https://exemple.com", new NewtonsoftJsonSerializer());
var sendLinkStatusUpdate = new GraphQLRequest
{
Query = "mutation{sendLinkStatusUpdate(input : {newLinkStatus: \"active\"}) {errors {scope message}}}",
Variables = new
{
newLinkStatus = "active"
}
};
var graphQLResponse = await graphQLClient.SendQueryAsync<sendLinkRequest>(sendLinkStatusUpdate);
}
Anyway, if someone has an idea or a lead I'm interested
Thanks for your comment ! I managed to do it like that :)
public sendLinkRequest sendLinkStatusUpdate(newLinkStatusInput input)
{
TrackingActionFilter ta = new TrackingActionFilter();
ta.ProcessCompleted += ta_nextAction; // register with an event
sendLinkRequest test = ta.StartProcess(input, _sendLinkRequestService);
return test;
}
public async static void ta_nextAction(object sender, EventArgs e)
{
var graphQLClient = new GraphQLHttpClient("https://talentusnewdemo.eu-west-1.beepleapp.eu/b/multi-tenant-api", new NewtonsoftJsonSerializer());
var sendLinkStatusUpdate = new GraphQLRequest
{
Query = "mutation{sendLinkStatusUpdate(input : {newLinkStatus: \"active\"}) {errors {scope message}}}",
Variables = new
{
newLinkStatus = "active"
}
};
var graphQLResponse = await graphQLClient.SendQueryAsync<sendLinkRequest>(sendLinkStatusUpdate);
Console.WriteLine("Process Completed!");
}
public class TrackingActionFilter
{
public event EventHandler ProcessCompleted;
public sendLinkRequest StartProcess(newLinkStatusInput input, IsendLinkRequestService _sendLinkRequestService)
{
Console.WriteLine("Process Started!");
OnProcessCompleted(EventArgs.Empty); //No event data
return _sendLinkRequestService.sendLinkStatusUpdate(input);
}
protected virtual void OnProcessCompleted(EventArgs e)
{
ProcessCompleted?.Invoke(this, e);
}
}
I'm building an app which will scrape some data from a website and shows a notification when some criteria are met.
Everything works well without problems when the app is open (because the WebView is being rendered) but when I close the app the WebView is disabled so I cannot use it to scrape data anymore.
The scraping code is inside a class called from a ForegroundService.
I've already looked on the internet but I'm unable to find a solution or a substitute to WebView, do you have any ideas?
I'm sorry if this question looks stupid to you, I've started to develop for mobile just one week ago
Below the JDMonitoring class which is called from the AlarmTask class
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace CGSJDSportsNotification {
public class JDMonitoring {
class Ticket {
string owner;
string title;
string store;
string lastUpdated;
string link;
public string ID { get; set; }
public string Owner {
get {
return owner == null ? "Nobody" : owner;
} set {
owner = value.Remove(0, value.IndexOf('(') + 1).Replace(")", "");
}
}
public string Title {
get {
return title;
} set {
if (value.StartsWith("(P"))
title = value.Remove(0, value.IndexOf(')') + 2);
}
}
public string Status { get; set; }
public string Store {
get {
return store;
} set {
store = value.Replace(#"\u003C", "").Replace(">", "");
}
}
public string LastUpdated {
get {
return lastUpdated;
} set {
string v;
int time = Convert.ToInt32(System.Text.RegularExpressions.Regex.Replace(value, #"[^\d]+", ""));
// Convert to minutes
if (value.Contains("hours"))
time *= 60;
v = time.ToString();
if (value.Contains("seconds"))
v = v.Insert(v.Length, " sec. ago");
else
v = v.Insert(v.Length, " min. ago");
lastUpdated = v;
}
}
public string Link {
get {
return link;
} set {
link = "https://support.jdplc.com/" + value;
}
}
}
public JDMonitoring() {
WB.Source = JDQueueMainUrl;
WB.Navigated += new EventHandler<WebNavigatedEventArgs>(OnNavigate);
}
IForegroundService FgService { get { return DependencyService.Get<IForegroundService>(); } }
WebView WB { get; } = MainPage.UI.MonitoringWebView;
string JDQueueMainUrl { get; } = "https://support.jdplc.com/rt4/Search/Results.html?Format=%27%3Cb%3E%3Ca%20href%3D%22__WebPath__%2FTicket%2FDisplay.html%3Fid%3D__id__%22%3E__id__%3C%2Fa%3E%3C%2Fb%3E%2FTITLE%3A%23%27%2C%0A%27%3Cb%3E%3Ca%20href%3D%22__WebPath__%2FTicket%2FDisplay.html%3Fid%3D__id__%22%3E__Subject__%3C%2Fa%3E%3C%2Fb%3E%2FTITLE%3ASubject%27%2C%0AStatus%2C%0AQueueName%2C%0AOwner%2C%0APriority%2C%0A%27__NEWLINE__%27%2C%0A%27__NBSP__%27%2C%0A%27%3Csmall%3E__Requestors__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__CreatedRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__ToldRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__LastUpdatedRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__TimeLeft__%3C%2Fsmall%3E%27&Order=DESC%7CASC%7CASC%7CASC&OrderBy=LastUpdated%7C%7C%7C&Query=Queue%20%3D%20%27Service%20Desk%20-%20CGS%27%20AND%20(%20%20Status%20%3D%20%27new%27%20OR%20Status%20%3D%20%27open%27%20OR%20Status%20%3D%20%27stalled%27%20OR%20Status%20%3D%20%27deferred%27%20OR%20Status%20%3D%20%27open%20-%20awaiting%20requestor%27%20OR%20Status%20%3D%20%27open%20-%20awaiting%20third%20party%27%20)&RowsPerPage=0&SavedChartSearchId=new&SavedSearchId=new";
bool MonitoringIsInProgress { get; set; } = false;
public bool IsConnectionAvailable {
get {
try {
using (new WebClient().OpenRead("http://google.com/generate_204"))
return true;
} catch {
return false;
}
}
}
async Task<bool> IsOnLoginPage() {
if (await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('left')[0].innerText") != null)
return true;
return false;
}
async Task<bool> Login() {
await WB.EvaluateJavaScriptAsync($"document.getElementsByName('user')[0].value = '{UserSettings.SecureEntries.Get("rtUser")}'");
await WB.EvaluateJavaScriptAsync($"document.getElementsByName('pass')[0].value = '{UserSettings.SecureEntries.Get("rtPass")}'");
await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('button')[0].click()");
await Task.Delay(1000);
// Checks for wrong credentials error
if (await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('action-results')[0].innerText") == null)
return true;
return false;
}
async Task<List<Ticket>> GetTickets() {
List<Ticket> tkts = new List<Ticket>();
// Queue tkts index (multiple of 2)
int index = 2;
// Iterates all the queue
while (await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].innerText") != null) {
Ticket tkt = new Ticket();
tkt.LastUpdated = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index + 1}].getElementsByTagName('td')[4].innerText");
// Gets only the tkts which are not older than the value selected by the user
if (Convert.ToInt32(System.Text.RegularExpressions.Regex.Replace(tkt.LastUpdated, #"[^\d]+", "")) > Convert.ToInt32(UserSettings.Entries.Get("searchTimeframe")))
break;
tkt.ID = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[0].innerText");
tkt.Owner = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[4].innerText");
tkt.Title = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[1].innerText");
tkt.Status = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[2].innerText");
tkt.Store = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index + 1}].getElementsByTagName('td')[1].innerText");
tkt.Link = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[1].getElementsByTagName('a')[0].getAttribute('href')");
tkts.Add(tkt);
index += 2;
}
return tkts;
}
//async Task<string> QueueGetTkt
async void OnNavigate(object sender, WebNavigatedEventArgs args) {
if (MonitoringIsInProgress)
return;
if (IsConnectionAvailable) {
if (await IsOnLoginPage()) {
if (await Login() == false) {
// If the log-in failed we can't proceed
MonitoringIsInProgress = false;
FgService.NotificationNewTicket("Log-in failed!", "Please check your credentials");
// Used to avoid an infinite loop of OnNavigate method calls
WB.Source = "about:blank";
return;
}
}
// Main core of the monitoring
List<Ticket> tkts = await GetTickets();
if (tkts.Count > 0) {
foreach(Ticket t in tkts) {
// Looks only after the tkts with the country selected by the user (and if it was selected by the user, also for the tkts without a visible country)
// Firstly we look in the title
if (t.Title.Contains(MainPage.UI.CountryPicker.SelectedItem.ToString())) {
FgService.NotificationNewTicket($"[{t.ID}] {t.LastUpdated}",
$"{t.Title}\r\n\r\n" +
$"Status: {t.Status}\r\n" +
$"Owner: {t.Owner}\r\n" +
$"Last updated: {t.LastUpdated}");
break;
}
}
}
}
MonitoringIsInProgress = false;
}
}
}
AlarmTask class
using Android.App;
using Android.Content;
using Android.Support.V4.App;
namespace CGSJDSportsNotification.Droid {
[BroadcastReceiver(Enabled = true, Exported = true, DirectBootAware = true)]
[IntentFilter(new string[] { Intent.ActionBootCompleted, Intent.ActionLockedBootCompleted, "android.intent.action.QUICKBOOT_POWERON", "com.htc.intent.action.QUICKBOOT_POWERON" }, Priority = (int)IntentFilterPriority.HighPriority)]
public class AlarmTask : BroadcastReceiver {
IAlarm _MainActivity { get { return Xamarin.Forms.DependencyService.Get<IAlarm>(); } }
public override void OnReceive(Context context, Intent intent) {
if (intent.Action != null) {
if (intent.Action.Equals(Intent.ActionBootCompleted)) {
// Starts the app after reboot
var serviceIntent = new Intent(context, typeof(MainActivity));
serviceIntent.AddFlags(ActivityFlags.NewTask);
context.StartActivity(serviceIntent);
Intent main = new Intent(Intent.ActionMain);
main.AddCategory(Intent.CategoryHome);
context.StartActivity(main);
// Does not work, app crashes on boot received
/*if (UserSettings.Entries.Exists("monitoringIsRunning")) {
if ((bool)UserSettings.Entries.Get("monitoringIsRunning"))
FgService.Start();
}*/
}
} else
// Checks for new tkts on a new thread
new JDMonitoring();
// Restarts the alarm
_MainActivity.AlarmStart();
}
// Called from JDMonitoring class
public static void NotificationNewTicket(string title, string message, bool icoUnknownCountry = false) {
new AlarmTask().NotificationShow(title, message, icoUnknownCountry);
}
void NotificationShow(string title, string message, bool icoUnknownCountry) {
int countryFlag = Resource.Drawable.newTktUnknownCountry;
if (icoUnknownCountry == false) {
switch (MainPage.UI.CountryPicker.SelectedItem.ToString()) {
case "Italy":
countryFlag = Resource.Drawable.newTktItaly;
break;
case "Spain":
countryFlag = Resource.Drawable.newTktSpain;
break;
case "Germany":
countryFlag = Resource.Drawable.newTktGermany;
break;
case "Portugal":
countryFlag = Resource.Drawable.newTktPortugal;
break;
}
}
var _intent = new Intent(Application.Context, typeof(MainActivity));
_intent.AddFlags(ActivityFlags.ClearTop);
_intent.PutExtra("jdqueue_notification", "extra");
var pendingIntent = PendingIntent.GetActivity(Application.Context, 0, _intent, PendingIntentFlags.OneShot);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(Application.Context, "newTktNotification_channel")
.SetVisibility((int)NotificationVisibility.Public)
.SetPriority((int)NotificationPriority.High)
.SetDefaults((int)NotificationDefaults.Sound | (int)NotificationDefaults.Vibrate | (int)NotificationDefaults.Lights)
.SetSmallIcon(Resource.Drawable.newTktNotification)
.SetLargeIcon(Android.Graphics.BitmapFactory.DecodeResource(Application.Context.Resources, countryFlag))
.SetSubText("Click to check the queue")
.SetStyle(new NotificationCompat.BigTextStyle()
.SetBigContentTitle("New ticket available!")
.BigText(message))
.SetContentText(title)
.SetAutoCancel(true)
.SetContentIntent(pendingIntent);
NotificationManagerCompat.From(Application.Context).Notify(0, notificationBuilder.Build());
}
}
}
And the ForegroundService class which is responsible to trigger for the first time the alarm
using Android.App;
using Android.Content;
using Android.OS;
namespace CGSJDSportsNotification.Droid {
[Service]
class ForegroundService : Service {
IAlarm _MainActivity { get { return Xamarin.Forms.DependencyService.Get<IAlarm>(); } }
public override IBinder OnBind(Intent intent) { return null; }
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId) {
// Starts the Foreground Service and the notification channel
StartForeground(9869, new ForegroundServiceNotification().ReturnNotif());
Android.Widget.Toast.MakeText(Application.Context, "JD Queue - Monitoring started!", Android.Widget.ToastLength.Long).Show();
_MainActivity.AlarmStart();
return StartCommandResult.Sticky;
}
public override void OnDestroy() {
Android.Widget.Toast.MakeText(Application.Context, "JD Queue - Monitoring stopped!", Android.Widget.ToastLength.Long).Show();
_MainActivity.AlarmStop();
UserSettings.Entries.AddOrEdit("monitoringIsRunning", false);
UserSettings.Entries.AddOrEdit("monitoringStopPending", false, false);
base.OnDestroy();
}
public override bool StopService(Intent name) {
return base.StopService(name);
}
}
}
Thank you!
[BETTER-FINAL-SOLUTION]
After several hours I've discovered Android WebView which does exactly what I need (I'm developing this app only for Android)
I've written this Browser helper class
class Browser {
public Android.Webkit.WebView WB;
static string JSResult;
public class CustomWebViewClient : WebViewClient {
public event EventHandler<bool> OnPageLoaded;
public override void OnPageFinished(Android.Webkit.WebView view, string url) {
OnPageLoaded?.Invoke(this, true);
}
}
public Browser(CustomWebViewClient wc, string url = "") {
WB = new Android.Webkit.WebView(Android.App.Application.Context);
WB.Settings.JavaScriptEnabled = true;
WB.SetWebViewClient(wc);
WB.LoadUrl(url);
}
public string EvalJS(string js) {
JSInterface jsi = new JSInterface();
WB.EvaluateJavascript($"javascript:(function() {{ return {js}; }})()", jsi);
return JSResult;
}
class JSInterface : Java.Lang.Object, IValueCallback {
public void OnReceiveValue(Java.Lang.Object value) {
JSResult = value.ToString();
}
}
}
[EDIT]
Improved the JS returning function with async callbacks (so the JS return value will be always delivered).
Credits to ChristineZuckerman
class Browser {
public Android.Webkit.WebView WB;
public class CustomWebViewClient : WebViewClient {
public event EventHandler<bool> OnPageLoaded;
public override void OnPageFinished(Android.Webkit.WebView view, string url) {
OnPageLoaded?.Invoke(this, true);
}
}
public Browser(CustomWebViewClient wc, string url = "") {
WB = new Android.Webkit.WebView(Android.App.Application.Context);
WB.ClearCache(true);
WB.Settings.JavaScriptEnabled = true;
WB.Settings.CacheMode = CacheModes.NoCache;
WB.Settings.DomStorageEnabled = true;
WB.Settings.SetAppCacheEnabled(false);
WB.Settings.UserAgentString = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10";
WB.LoadUrl(url);
WB.SetWebViewClient(wc);
}
public async Task<string> EvalJS(string js, bool returnNullObjectWhenNull = true) {
string JSResult = "";
ManualResetEvent reset = new ManualResetEvent(false);
Device.BeginInvokeOnMainThread(() => {
WB?.EvaluateJavascript($"javascript:(function() {{ return {js}; }})()", new JSInterface((r) => {
JSResult = r;
reset.Set();
}));
});
await Task.Run(() => { reset.WaitOne(); });
return JSResult == "null" ? returnNullObjectWhenNull ? null : "null" : JSResult;
}
class JSInterface : Java.Lang.Object, IValueCallback {
private Action<string> _callback;
public JSInterface(Action<string> callback) {
_callback = callback;
}
public void OnReceiveValue(Java.Lang.Object value) {
string v = value.ToString();
if (v.StartsWith('"') && v.EndsWith('"'))
v = v.Remove(0, 1).Remove(v.Length - 2, 1);
_callback?.Invoke(v);
}
}
}
Example:
Browser.CustomWebViewClient wc = new Browser.CustomWebViewClient();
wc.OnPageLoaded += BrowserOnPageLoad;
Browser browser = new Browser(wc, "https://www.google.com/");
void BrowserOnPageLoad(object sender, bool e) {
string test = browser.EvalJS("document.getElementsByClassName('Q8LRLc')[0].innerText");
// 'test' will contain the value returned from the JS script
// You can acces the real WebView object by using
// browser.WB
}
// OR WITH THE NEW RETURNING FUNCTION
async void BrowserOnPageLoad(object sender, bool e) {
string test = await browser.EvalJS("document.getElementsByClassName('Q8LRLc')[0].innerText");
// 'test' will contain the value returned from the JS script
// You can acces the real WebView object by using
// browser.WB
}
[FINAL-SOLUTION]
Finally I've found an easy and efficient alternative to WebView.
Now I'm using SimpleBroswer and works great!
[SEMI-SOLUTION]
Alright, I've written a workaround but I don't really like this idea, so please, if you know a better method let me know.
Below my workaround:
In my ForegroundServiceHelper interface I've added a method to check if the MainActivity (where the WebView it's rendered) is visible or not, if isn't visible the MainActivity will be shown and immediately will be hidden back.
And the app will be removed from the last used applications
Method inside my ForegroundServiceHelper Interface
public void InitBackgroundWebView() {
if ((bool)SharedSettings.Entries.Get("MainPage.IsVisible") == false) {
// Shows the activity
Intent serviceIntent = new Intent(context, typeof(MainActivity));
serviceIntent.AddFlags(ActivityFlags.NewTask);
context.StartActivity(serviceIntent);
// And immediately hides it back
Intent main = new Intent(Intent.ActionMain);
main.AddFlags(ActivityFlags.NewTask);
main.AddCategory(Intent.CategoryHome);
context.StartActivity(main);
// Removes from the last app used
ActivityManager am = (new ContextWrapper(Android.App.Application.Context)).GetSystemService(Context.ActivityService).JavaCast<ActivityManager>();
if (am != null) {
System.Collections.Generic.IList<ActivityManager.AppTask> tasks = am.AppTasks;
if (tasks != null && tasks.Count > 0) {
tasks[0].SetExcludeFromRecents(true);
}
}
}
}
The SharedSettings class is an helper class wrapped around the App.Current.Properties Dictionary
And in the OnAppearing and OnDisappearing callbacks I set the shared values to true/false
[EDIT]
This workaround works only if the user is on the homepage, so I need to find an another solution...
I execute a lot of request to some the resources with HttpClient.
To avoid licks I use it as single instance. Something like that...
I want to use proxy, so how I can use different proxies for each request?
Thanks!
public class Program
{
private static HttpClient Client = new HttpClient();
public static void Main(string[] args)
{
Console.WriteLine("Starting connections");
for(int i = 0; i<10; i++)
{
var result = Client.GetAsync("http://aspnetmonsters.com").Result;
Console.WriteLine(result.StatusCode);
}
Console.WriteLine("Connections done");
Console.ReadLine();
}
}
Ah, I misread the question.
It's not about how to use a random IWebProxy with HttpClientHandler, but how to work around being unable to reset the proxy property of the same HttpClientHandler once the first request has started.
The problem is that you can't reset the proxy of the HttpClientHandler...
System.InvalidOperationException: 'This instance has already started
one or more requests.
Properties can only be modified before sending the first request.'
But it's still rather easy.
Proxy property of HttpClientHandler takes an object that implements IWebProxy.
IWebProxy interface has a method GetProxy that return the Uri of the proxy.
So you can make your own class that implements this interface and control how it returns the Uri of the proxy with GetProxy.
You can make it wrap another IWebProxy, and in GetProxy it would return GetProxy of the inner IWebProxy.
This way, you won't have to change the Proxy property of the HttpClientHandler, you can just change the inner IWebProxy.
Implementation:
public class WebProxyService
: System.Net.IWebProxy
{
protected System.Net.IWebProxy m_proxy;
public System.Net.IWebProxy Proxy
{
get { return this.m_proxy ??= System.Net.WebRequest.DefaultWebProxy; }
set { this.m_proxy = value; }
}
System.Net.ICredentials System.Net.IWebProxy.Credentials
{
get { return this.Proxy.Credentials; }
set { this.Proxy.Credentials = value; }
}
public WebProxyService()
{ } // Constructor
public WebProxyService(System.Net.IWebProxy proxy)
{
this.Proxy = proxy;
} // Constructor
System.Uri System.Net.IWebProxy.GetProxy(System.Uri destination)
{
return this.Proxy.GetProxy(destination);
}
bool System.Net.IWebProxy.IsBypassed(System.Uri host)
{
return this.Proxy.IsBypassed(host);
}
}
And then usage goes like this:
public class AlternatingProxy
{
public static async System.Threading.Tasks.Task Test()
{
string url = "http://aspnetmonsters.com";
System.Net.WebProxy[] proxies = new[] {
null,
new System.Net.WebProxy("104.238.172.20", 8080),
new System.Net.WebProxy("104.238.167.193", 8080),
new System.Net.WebProxy("136.244.102.38", 8080),
new System.Net.WebProxy("95.179.202.40", 8080)
};
System.Random rnd = new System.Random();
WebProxyService proxyService = new WebProxyService();
using (System.Net.Http.HttpClient hc = new System.Net.Http.HttpClient(
new System.Net.Http.HttpClientHandler { UseProxy = true, Proxy = proxyService }
))
{
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
hc.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148");
hc.DefaultRequestHeaders.Add("Accept-Language", "fr-FR, fr;q=0.9, en;q=0.8, it;q=0.7, *;q=0.5");
hc.DefaultRequestHeaders.Add("Referer", "https://www.baidu.com");
hc.DefaultRequestHeaders.ConnectionClose = true;
for (int i = 0; i < 10; ++i)
{
proxyService.Proxy = proxies[rnd.Next(proxies.Length)];
string response = await hc.GetStringAsync(url);
}
}
} // End Task Test
} // End Class TestMe
Edit:
If you want to use a singleton, maybe this is an idea:
public class WebProxyService
: System.Net.IWebProxy
{
protected System.Net.IWebProxy m_proxy;
public System.Net.IWebProxy Proxy
{
get { return this.m_proxy ??= System.Net.WebRequest.DefaultWebProxy; }
set { this.m_proxy = value; }
}
System.Net.ICredentials System.Net.IWebProxy.Credentials
{
get { return this.Proxy.Credentials; }
set { this.Proxy.Credentials = value; }
}
public WebProxyService()
{ } // Constructor
public WebProxyService(System.Net.IWebProxy proxy)
{
this.Proxy = proxy;
} // Constructor
protected System.Func<System.Net.WebProxy>[] proxies = new System.Func<System.Net.WebProxy>[] {
delegate(){ return new System.Net.WebProxy("104.238.172.20", 8080); },
delegate (){ return new System.Net.WebProxy("104.238.167.193", 8080);},
delegate(){ return new System.Net.WebProxy("136.244.102.38", 8080);},
delegate(){ return new System.Net.WebProxy("95.179.202.40", 8080);}
};
System.Uri System.Net.IWebProxy.GetProxy(System.Uri destination)
{
return proxies[RandomGen2.Next(proxies.Length)]().GetProxy(destination);
}
bool System.Net.IWebProxy.IsBypassed(System.Uri host)
{
return this.Proxy.IsBypassed(host);
}
private static class RandomGen2
{
private static System.Random _global = new System.Random();
[System.ThreadStatic]
private static System.Random _local;
public static int Next(int maxValue)
{
System.Random inst = _local;
if (inst == null)
{
int seed;
lock (_global) seed = _global.Next();
_local = inst = new System.Random(seed);
}
return inst.Next(maxValue);
}
}
} // End Class WebProxyService
Edit 2:
It's still not thread-safe if you change the proxy.
Therefore, using a fixed immutable proxy-list and blocking the set-property.
This way, it should be thread-safe.
public class WebProxyService
: System.Net.IWebProxy
{
protected System.Net.IWebProxy[] m_proxyList;
public System.Net.IWebProxy Proxy
{
get
{
// https://devblogs.microsoft.com/pfxteam/getting-random-numbers-in-a-thread-safe-way/
if (this.m_proxyList != null)
return this.m_proxyList[ThreadSafeRandom.Next(this.m_proxyList.Length)];
return System.Net.WebRequest.DefaultWebProxy;
}
set
{
throw new System.InvalidOperationException("It is not thread-safe to change the proxy-list.");
}
}
System.Net.ICredentials System.Net.IWebProxy.Credentials
{
get { return this.Proxy.Credentials; }
set { this.Proxy.Credentials = value; }
}
public WebProxyService()
{
} // Constructor
public WebProxyService(System.Net.IWebProxy[] proxyList)
{
this.m_proxyList = proxyList;
} // Constructor
System.Uri System.Net.IWebProxy.GetProxy(System.Uri destination)
{
return this.Proxy.GetProxy(destination);
}
bool System.Net.IWebProxy.IsBypassed(System.Uri host)
{
return this.Proxy.IsBypassed(host);
}
} // End Class WebProxyService
Old answer:
----
Using a proxy with HttpClient in ASP.NET-Core is actually quite simple.
All you need to do is set the handler in the HttpClient-constructor.
Then set the proxy-property of the handler for each request.
Like this:
public class Program
{
public static async System.Threading.Tasks.Task Main(string[] args)
{
string url = "http://aspnetmonsters.com";
System.Net.WebProxy[] proxies = new[] {
null,
new System.Net.WebProxy("104.238.172.20", 8080),
new System.Net.WebProxy("104.238.167.193", 8080),
new System.Net.WebProxy("136.244.102.38", 8080),
new System.Net.WebProxy("95.179.202.40", 8080)
};
System.Random rnd = new System.Random();
using (System.Net.Http.HttpClientHandler handler = new System.Net.Http.HttpClientHandler()
{
Proxy = new System.Net.WebProxy("http://127.0.0.1:8888"),
UseProxy = true,
})
{
using (System.Net.Http.HttpClient hc = new System.Net.Http.HttpClient(handler))
{
System.Console.WriteLine("Starting connections");
for (int i = 0; i < 10; i++)
{
handler.Proxy = proxies[rnd.Next(proxies.Length)];
await hc.GetAsync(url);
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
hc.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148");
hc.DefaultRequestHeaders.Add("Accept-Language", "fr-FR, fr;q=0.9, en;q=0.8, it;q=0.7, *;q=0.5");
hc.DefaultRequestHeaders.Add("Referer", "https://www.baidu.com");
using (System.Net.Http.HttpResponseMessage response = await hc.GetAsync(url))
{
// using (var fs = new System.IO.MemoryStream())
// { await response.Content.CopyToAsync(fs); }
byte[] ba = await response.Content.ReadAsByteArrayAsync();
} // End Using response
} // Next i
System.Console.WriteLine("Ending connections");
} // End Using hc
} // End Using handler
System.Console.WriteLine("--- Press any key to continue --- ");
System.Console.ReadKey();
} // End Task Main
} // End Class Program
You will need to implement IWebProxy.
here is a very row sample.
First implement IWebProxy
public class MyProxy : IWebProxy {
public MyProxy() { credentials = new NetworkCredential( user, password ); }
private NetworkCredential credentials;
public ICredentials Credentials
{
get = > credentials;
set = > throw new NotImplementedException();
}
private Uri proxyUri;
public Uri GetProxy( Uri destination )
{
return proxyUri; // your proxy Uri
}
public bool IsBypassed( Uri host )
{
return false;
}
private const string user = "yourusername";
private const string password = "password";}
Then provide it to handler in HttpClient
public class MyHttpClient {
internal static HttpResult httpMethod( ... )
{
var _client = client();
try
{
var message = new HttpRequestMessage( method, url );
message.Content = new StringContent( content, Encoding.UTF8, "application/json" );
var result = _client.SendAsync( message ).Result;// handle result
}
catch( Exception e ){}
}
private static HttpClient client()
{
var httpClientHandler = new HttpClientHandler() { Proxy = new MyProxy() };
var httpClient = new MyClient( new Uri( "baseurl" ), httpClientHandler );
return httpClient;
So basically to be able to change proxies you will need a reference on the HttpClientHandler.
A simple example can be found here: C# use proxy with HttpClient request
and another one here: Simple C# .NET 4.5 HTTPClient Request Using Basic Auth and Proxy
I would suggest to keep the HttpClientHandler on a private field and use the reference to change the proxy everytime you need it.
Keep in mind though that if you need to use different proxies simultaneously you would need to have multiple instances of the HttpClientHandler class.
If you need me to make a sample code for that. Ping me.
Thank you.
I have a simple code that make authentication in Platform-specific code:
[assembly: ExportRenderer(typeof(FacebookLoginPage), typeof(FacebookLoginRenderer))]
namespace VejoSeriesMobile.Droid.Renderers
{
public class FacebookLoginRenderer : PageRenderer
{
public static string ClientId = "";
public static string ClientSecret = "";
public static string Scope = "email, public_profile";
public static string AuthorizeUrl = "https://m.facebook.com/dialog/oauth";
public static string RedirectUrl = "https://www.facebook.com/connect/login_success.html";
public static string AccessTokenUrl = "https://m.facebook.com/dialog/oauth/token";
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
var activity = this.Context as Activity;
var auth = new OAuth2Authenticator(
clientId: "717427648360004",
scope: Scope,
authorizeUrl: new Uri("https://m.facebook.com/dialog/oauth/"),
redirectUrl: new Uri("http://www.facebook.com/connect/login_success.html"));
auth.Completed += OnAuthenticationCompleted;
activity.StartActivity(auth.GetUI(activity));
}
async void OnAuthenticationCompleted(object sender, AuthenticatorCompletedEventArgs e)
{
if (e.IsAuthenticated)
{
var request = new OAuth2Request("GET", new Uri("https://graph.facebook.com/me?fields=id,name,picture,email"), null, e.Account);
await request.GetResponseAsync().ContinueWith(t =>
{
var fbUser = JObject.Parse(t.Result.GetResponseText());
var id = fbUser["id"].ToString().Replace("\"", "");
var name = fbUser["name"].ToString().Replace("\"", "");
var email = fbUser["email"].ToString().Replace("\"", "");
var conta = new Account { Username = name };
conta.Properties.Add("email", email);
AccountStore.Create(Context).Save(conta, "VejoSeries");
App.SuccessfulLoginAction.Invoke();
});
}
}
}
}
in my App portable class I have a static method that is calling after login complete:
But in this scope I only can use Static method, I wish to call a non static method for re rendering the entire Page (with menus, etc..) after login. How can I do this?
Above my successLoginAction:
public static Action SuccessfulLoginAction
{
get
{
return new Action(() =>
{
//var masterDetailPage = Application.Current.MainPage as MasterDetailPage;
//masterDetailPage.Detail = new NavigationPage((Page)Activator.CreateInstance(typeof(Painel)));
//masterDetailPage.IsPresented = false;
_NavPage.Navigation.PopAsync();
_NavPage.Navigation.PushAsync(new MainPage());
});
}
}
The short answer is don't make it static.
If you change it to just a public property, you can access it through App.Current.
To be more specific:
public Action SuccessfulLoginAction
{
get
{
return new Action(() =>
{
//var masterDetailPage = Application.Current.MainPage as MasterDetailPage;
//masterDetailPage.Detail = new NavigationPage((Page)Activator.CreateInstance(typeof(Painel)));
//masterDetailPage.IsPresented = false;
_NavPage.Navigation.PopAsync();
_NavPage.Navigation.PushAsync(new MainPage());
});
}
}
Then call it from anywhere like this:
((App)Application.Current).SuccessfulLoginAction.Invoke();
That way you can access your _NavPage as the whole thing is bound to the instance of App.
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);
}
}