I have a WebAPI controller with a custom CORS policy provider attribute on the class. In defining the attribute, I have the following constructor.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class ConfiguredCORSPolicyProviderAttribute : ActionFilterAttribute, ICorsPolicyProvider
{
private CorsPolicy _policy;
public ConfiguredCORSPolicyProviderAttribute()
{
_policy = new CorsPolicy
{
AllowAnyMethod = true,
AllowAnyHeader = true
};
// If there are no domains in the 'CORSDomainSection' section in Web.config, all origins will be allowed by default.
var domains = (CORSDomainSection)ConfigurationManager.GetSection("CORSDomainSection");
if (domains != null)
{
foreach (DomainConfigElement domain in domains.Domains)
{
_policy.Origins.Add(domain.Domain);
}
}
else
{
_policy.AllowAnyOrigin = true;
}
}
public Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request)
{
return Task.FromResult(_policy);
}
public Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken token)
{
return GetCorsPolicyAsync(request);
}
}
The ConfigurationManager gets a list (from Web.config) of acceptable origins/domains that I want to allow to make requests.
This code appropriately handles the "Access-Control-Allow-Origin" header, adding it when the request origin is on the list, and withholding it when not. However, the code in the controller still gets called no matter what.
Why, and how do I appropriately prevent the controller from executing if the origin of the request isn't allowed?
UPDATE || [April 12, 2016 # 12:30p]
I was able to resolve the issue using a combination of OnActionExecuted and OnActionExecuting method overrides, code below.
/// <summary>
/// Executed after the action method is invoked.
/// </summary>
/// <param name="context">The context of the HTTP request.</param>
public override void OnActionExecuted(HttpActionExecutedContext context)
{
string requestOrigin;
try
{
requestOrigin = context.Request.Headers.GetValues("Origin").FirstOrDefault();
}
catch
{
requestOrigin = string.Empty;
}
if (IsAllowedOrigin(requestOrigin))
{
context.Response.Headers.Add("Access-Control-Allow-Origin", requestOrigin);
if (IsPreflight(context))
{
string allowedMethods = string.Empty;
string allowedHeaders = string.Empty;
if (Policy.AllowAnyMethod)
{
allowedMethods = context.Request.Headers.GetValues("Access-Control-Request-Method").FirstOrDefault();
}
else
{
foreach (var method in Policy.Methods)
{
if (Policy.Methods.IndexOf(method) == 0)
{
allowedMethods = method;
}
else
{
allowedMethods += string.Format(", {0}", method);
}
}
}
try
{
if (Policy.AllowAnyHeader)
{
allowedHeaders = context.Request.Headers.GetValues("Access-Control-Request-Headers").FirstOrDefault();
}
else
{
foreach (var header in Policy.Headers)
{
if (Policy.Headers.IndexOf(header) == 0)
{
allowedHeaders = header;
}
else
{
allowedHeaders += string.Format(", {0}", header);
}
}
}
context.Response.Headers.Add("Access-Control-Allow-Headers", allowedHeaders);
}
catch
{
// Do nothing.
}
context.Response.Headers.Add("Access-Control-Allow-Methods", allowedMethods);
}
}
base.OnActionExecuted(context);
}
/// <summary>
/// Executed before the action method is invoked.
/// </summary>
/// <param name="context">The context of the HTTP request.</param>
public override void OnActionExecuting(HttpActionContext context)
{
string requestOrigin;
try
{
requestOrigin = context.Request.Headers.GetValues("Origin").FirstOrDefault();
}
catch
{
requestOrigin = string.Empty;
}
if (IsAllowedOrigin(requestOrigin))
{
base.OnActionExecuting(context);
}
else
{
context.ModelState.AddModelError("State", "The origin of the request is forbidden from making requests.");
context.Response = context.Request.CreateErrorResponse(HttpStatusCode.Forbidden, context.ModelState);
}
}
private bool IsAllowedOrigin(string requestOrigin)
{
requestOrigin = requestOrigin.Replace("https://", "").Replace("http://", "");
if (System.Diagnostics.Debugger.IsAttached || PolicyContains(requestOrigin))
{
return true;
}
else
{
return false;
}
}
private bool PolicyContains(string requestOrigin)
{
foreach (var domain in _policy.Origins)
{
if (domain.Replace("https://", "").Replace("http://", "") == requestOrigin)
{
return true;
}
}
return false;
}
private bool IsPreflight(HttpActionExecutedContext context)
{
string header = string.Empty;
try
{
header = context.Request.Headers.GetValues("Access-Control-Request-Method").FirstOrDefault();
}
catch
{
return false;
}
if (header != null && context.Request.Method == HttpMethod.Options)
{
return true;
}
else
{
return false;
}
}
CORS headers are not expected to prevent calls to controller - i.e. if native client (pretty much anything that is not a browser) calls the method it should be handled by default.
If you really need to block such calls - perform similar checks in before controller get called in OnActionExecutingAsync.
Related
My use case:
In a single threaded application, I need to serialize arbitrary classes for logging purposes.
The arbitrary classes are predominantly translated in an automated way from a massive VB6 application into .NET.
If serialized without a timeout, the serialization method will loop until it runs out of memory.
This is what I have currently:
internal class Serializer
{
private readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public volatile string result = null;
public volatile Func<string> toExecute = null;
public Thread thread;
public ManualResetEventSlim messageToSender = new ManualResetEventSlim(false);
public ManualResetEventSlim messageToReceiver = new ManualResetEventSlim(false);
public Serializer()
{
thread = new Thread(new ThreadStart(run));
thread.Start();
}
~Serializer()
{
try
{
if (messageToSender != null) messageToSender.Dispose();
}
catch { };
try
{
if (messageToReceiver != null) messageToReceiver.Dispose();
}
catch { };
}
public volatile bool ending = false;
public void run()
{
while (!ending)
{
try
{
if (toExecute != null)
{
result = toExecute();
}
messageToReceiver.Reset();
messageToSender.Set();
messageToReceiver.Wait();
}
catch (ThreadInterruptedException)
{
log.Warn("Serialization interrupted");
break;
}
catch (ThreadAbortException)
{
Thread.ResetAbort();
result = null;
}
catch (Exception ex)
{
log.Error("Error in Serialization", ex);
Console.WriteLine(ex);
break;
}
}
}
}
public class LocalStructuredLogging
{
private static volatile Serializer _serializer;
private static Serializer serializer
{
get
{
if (_serializer == null)
{
_serializer = new Serializer();
}
return _serializer;
}
}
public void LogStucturedEnd()
{
try
{
if (serializer != null)
{
serializer.ending = true;
serializer.thread.Interrupt();
}
}
catch { }
}
internal ConcurrentDictionary<long, bool> disallowedToSerialize = new ConcurrentDictionary<long, bool>();
public string TrySerialize<T>(T payload, [CallerLineNumber] int line = 0)
{
long hashEl = typeof(T).Name.GetHashCode() * line;
bool dummy;
unchecked
{
if (disallowedToSerialize.TryGetValue(hashEl, out dummy))
{
return "°,°";
}
}
serializer.toExecute = () =>
{
try
{
return Newtonsoft.Json.JsonConvert.SerializeObject(payload, new Newtonsoft.Json.JsonSerializerSettings() { ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore });
}
catch (Exception)
{
disallowedToSerialize.TryAdd(hashEl, false);
return "°°°";
}
};
try
{
serializer.messageToSender.Reset();
serializer.messageToReceiver.Set();
if (serializer.messageToSender.Wait(6000))
{
return Interlocked.Exchange(ref serializer.result, null);
}
serializer.toExecute = null;
serializer.thread.Abort();
serializer.messageToSender.Wait(2000);
disallowedToSerialize.TryAdd(hashEl, false);
return "°§°";
}
catch (Exception)
{
disallowedToSerialize.TryAdd(hashEl, false);
return "°-°";
}
}
}
The code is called as in the following (test is an arbitrary class instance):
var logger = new LocalStructuredLogging();
var rr5 = logger.TrySerialize(test);
Although it seems to do the job, there are some issues with it:
it has a dependency on Thread.Abort
it is time dependent, so it will thus produce varied results on a loaded system
every class instance is treated like every other class instance - no tweaking
...
So, are there any better solutions available ?
Based upon dbc's excellent answer, I managed to create a better timed serializer.
It resolves all 3 issues mentioned above:
public class TimedJsonTextWriter : JsonTextWriter
{
public int? MaxDepth { get; set; }
public TimeSpan? MaxTimeUsed { get; set; }
public int MaxObservedDepth { get; private set; }
private DateTime start = DateTime.Now;
public TimedJsonTextWriter(TextWriter writer, JsonSerializerSettings settings, TimeSpan? maxTimeUsed)
: base(writer)
{
this.MaxDepth = (settings == null ? null : settings.MaxDepth);
this.MaxObservedDepth = 0;
this.MaxTimeUsed = maxTimeUsed;
}
public TimedJsonTextWriter(TextWriter writer, TimeSpan? maxTimeUsed, int? maxDepth = null)
: base(writer)
{
this.MaxDepth = maxDepth;
this.MaxTimeUsed = maxTimeUsed;
}
public override void WriteStartArray()
{
base.WriteStartArray();
CheckDepth();
}
public override void WriteStartConstructor(string name)
{
base.WriteStartConstructor(name);
CheckDepth();
}
public override void WriteStartObject()
{
base.WriteStartObject();
CheckDepth();
}
uint checkDepthCounter = 0;
private void CheckDepth()
{
MaxObservedDepth = Math.Max(MaxObservedDepth, Top);
if (Top > MaxDepth)
throw new JsonSerializationException($"Depth {Top} Exceeds MaxDepth {MaxDepth} at path \"{Path}\"");
unchecked
{
if ((++checkDepthCounter & 0x3ff) == 0 && DateTime.Now - start > MaxTimeUsed)
throw new JsonSerializationException($"Time Usage Exceeded at path \"{Path}\"");
}
}
}
public class LocalStructuredLogging
{
public void LogStucturedEnd()
{
}
internal HashSet<long> disallowedToSerialize = new HashSet<long>();
public string TrySerialize<T>(T payload, int maxDepth = 100, int secondsToTimeout = 2, [CallerLineNumber] int line = 0)
{
long hashEl = typeof(T).Name.GetHashCode() * line;
if (disallowedToSerialize.Contains(hashEl))
{
return "°,°";
}
try
{
var settings = new JsonSerializerSettings { MaxDepth = maxDepth, ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore };
using (var writer = new StringWriter())
{
using (var jsonWriter = new TimedJsonTextWriter(writer, settings, new TimeSpan(0, 0, secondsToTimeout)))
{
JsonSerializer.Create(settings).Serialize(jsonWriter, payload);
// Log the MaxObservedDepth here, if you want to.
}
return writer.ToString();
}
}
catch (Exception)
{
disallowedToSerialize.Add(hashEl);
return "°-°";
}
}
}
The only issue remaining are the Hash collisions, which are easy to solve (e.g. by using the source file name as well or use another type of Collection).
The correct way to run an action timed would be to do something like the following. I would recommend taking a second look at how serialization should work as well :).
/// <summary>
/// Run an action timed.
/// </summary>
/// <param name="action">Action to execute timed.</param>
/// <param name="secondsTimout">Seconds before Task should cancel.</param>
/// <returns></returns>
public static async Task RunTimeout(Action action, int secondsTimout) {
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(secondsTimout));
await Task.Run(action, tokenSource.Token);
}
You may also want to return a variable upon the completion of your timed task. That can be done like so...
public static async Task<T> RunTimeout<T>(Func<T> action, int secondsTimout) {
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(secondsTimout));
var result = await Task.Run(action, tokenSource.Token);
return result;
}
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 want to make some methods of my API to be locked (HttpStatus.Conflict) until another one with same params not finished (like ?id=1&key=sd6gd0f1g5ds16fh), like if bad user try to make 2+ same request at once, only one will be done.
My idea was to use Semaphore:
public class Lock : IDisposable
{
private bool _disposed = false;
private readonly Semaphore _semaphore;
public bool IsLocked
{
get;
private set;
}
public Lock(string name)
{
this.IsLocked = false;
try
{
this._semaphore = Semaphore.OpenExisting(name);
this._semaphore.Close();
}
catch (Exception)
{
this._semaphore = new Semaphore(0, 1, name);
this.IsLocked = true;
}
}
~Lock()
{
this.Dispose(false);
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing)
{
this._semaphore.Release();
this._semaphore.Dispose();
}
this._disposed = true;
}
}
}
I'm using it like this:
[ActionName("Ping")]
[HttpGet]
public IHttpActionResult Ping([FromUri]int? id = null, [FromUri]string key = null)
{
if (id == null)
{
//ProcessException is some wrap for api error answer
throw new ProcessException(HttpStatusCode.BadRequest, "Service ID is required");
}
if (key == null)
{
throw new ProcessException(HttpStatusCode.BadRequest, "Service Key is required");
}
Lock serviceLock = new Lock("service." + id + "." + key);
if (!serviceLock.IsLocked)
{
throw new ProcessException(HttpStatusCode.Conflict, "Other Service operation already in progress");
}
var service = Service.Get((int)id, key);
if (service == null) // Right hereino
{
throw new ProcessException(HttpStatusCode.Forbidden, "Service ID and/or Key is invalid");
}
Service.Touch((int)id);
serviceLock.Dispose();
//JResponse is some wrap for Dictionary<string, object>
return Ok(new JResponse(true));
}
But I'm pretty new to it and have some questions:
Am I going in true direction?
When I'm calling Dispose, Semaphore still exists on next request. What's wrong?
Will my class be disposed (and Semaphore released) on some exception? (Like we can see above, if service == null)
This is not perfect and there is room for improvement but thought it might start you off an another direction or way to thinking.
Work your Semaphore stuff into locking the static dictionary
//ToDo: You would have to make this ThreadSafe
public static class Helper
{
public static Dictionary<string,ClientDto> ClientDtos
= new Dictionary<string, ClientDto>();
}
public class ClientDto
{
public int ClientKey { get; set; }
public string Key { get; set; }
public DateTime CreatedOn { get; set; }
}
in your Global.asax add.
protected void Application_EndRequest()
{
Helper.ClientDtos.Remove(SeesionId);
}
//if this is called twice by the same client and the request is
//not finished processing the first request the second one will go into
//RequestBeingHandled and just return preventing the code from preforming
//the same action until the first/current is complete.
public IHttpActionResult Ping([FromUri]int? id = null, [FromUri]string key = null)
{
if(RequestBeingHandled(id, key))
{
//
Return .....
}
else
{
//if not add
ClientDto client = new ClientDto();
client.ClientKey = id;
client.Key = key;
client.CreatedOn = DateTime.Now;
Helper.ClientDtos.Add(SeesionId, client);
}
//call some code to do stuff...
}
private bool RequestBeingHandled(int id, string key)
{
//ToDo: write this code.
//check if its already in the dic
return bool;
}
We have a setup that used to work and has stopped sometime over the past few months. We use a custom dead-letter queue that is specified in config but basically gets set like this::
MsmqIntegrationBinding msmq = new MsmqIntegrationBinding(
// Defaults to WindowsDomain. We want all messages to be authenticated.
MsmqIntegrationSecurityMode.Transport);
msmq.DeadLetterQueue = DeadLetterQueue.Custom;
msmq.CustomDeadLetterQueue = new Uri("net.msmq://localhost/private/BulkUpdatesDeadLetter");
We've started getting this error, which seems to be pretty clear:
System.InvalidOperationException: A mismatch occurred between the binding and the MSMQ configuration. Messages cannot be sent. The custom dead letter queue specified in the binding must be a transactional queue. Ensure that the custom dead letter queue address is correct and the queue is a transactional queue.
We've verified both that we are pointing to the correct queue in the config and that the queue is transactional. Are there any other issues that might cause this exception to be thrown, or are we just missing something in the obvious?
Update: Had our web ops team delete and recreate the queues and still receiving the error.
The following code is provided by Microsoft corporation in the article BindingsSection.cs source code in C# .NET
namespace System.ServiceModel.Configuration
{
using System.Configuration;
using System.ServiceModel.Channels;
using System.ServiceModel;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Xml;
using System.Security;
public sealed partial class BindingsSection : ConfigurationSection, IConfigurationContextProviderInternal
{
static Configuration configuration = null;
ConfigurationPropertyCollection properties;
public BindingsSection() { }
Dictionary<string, bindingcollectionelement=""> BindingCollectionElements
{
get
{
Dictionary<string, bindingcollectionelement=""> bindingCollectionElements = new Dictionary<string, bindingcollectionelement="">();
foreach (ConfigurationProperty property in this.Properties)
{
bindingCollectionElements.Add(property.Name, this[property.Name]);
}
return bindingCollectionElements;
}
}
new public BindingCollectionElement this[string binding]
{
get
{
return (BindingCollectionElement)base[binding];
}
}
protected override ConfigurationPropertyCollection Properties
{
get
{
if (this.properties == null)
{
this.properties = new ConfigurationPropertyCollection();
}
this.UpdateBindingSections();
return this.properties;
}
}
[ConfigurationProperty(ConfigurationStrings.BasicHttpBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public BasicHttpBindingCollectionElement BasicHttpBinding
{
get { return (BasicHttpBindingCollectionElement)base[ConfigurationStrings.BasicHttpBindingCollectionElementName]; }
}
// This property should only be called/set from BindingsSectionGroup TryAdd
static Configuration Configuration
{
get { return BindingsSection.configuration; }
set { BindingsSection.configuration = value; }
}
[ConfigurationProperty(ConfigurationStrings.CustomBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public CustomBindingCollectionElement CustomBinding
{
get { return (CustomBindingCollectionElement)base[ConfigurationStrings.CustomBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.MsmqIntegrationBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public MsmqIntegrationBindingCollectionElement MsmqIntegrationBinding
{
get { return (MsmqIntegrationBindingCollectionElement)base[ConfigurationStrings.MsmqIntegrationBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.NetPeerTcpBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public NetPeerTcpBindingCollectionElement NetPeerTcpBinding
{
get { return (NetPeerTcpBindingCollectionElement)base[ConfigurationStrings.NetPeerTcpBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.NetMsmqBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public NetMsmqBindingCollectionElement NetMsmqBinding
{
get { return (NetMsmqBindingCollectionElement)base[ConfigurationStrings.NetMsmqBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.NetNamedPipeBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public NetNamedPipeBindingCollectionElement NetNamedPipeBinding
{
get { return (NetNamedPipeBindingCollectionElement)base[ConfigurationStrings.NetNamedPipeBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.NetTcpBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public NetTcpBindingCollectionElement NetTcpBinding
{
get { return (NetTcpBindingCollectionElement)base[ConfigurationStrings.NetTcpBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.WSFederationHttpBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public WSFederationHttpBindingCollectionElement WSFederationHttpBinding
{
get { return (WSFederationHttpBindingCollectionElement)base[ConfigurationStrings.WSFederationHttpBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.WS2007FederationHttpBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public WS2007FederationHttpBindingCollectionElement WS2007FederationHttpBinding
{
get { return (WS2007FederationHttpBindingCollectionElement)base[ConfigurationStrings.WS2007FederationHttpBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.WSHttpBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public WSHttpBindingCollectionElement WSHttpBinding
{
get { return (WSHttpBindingCollectionElement)base[ConfigurationStrings.WSHttpBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.WS2007HttpBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public WS2007HttpBindingCollectionElement WS2007HttpBinding
{
get { return (WS2007HttpBindingCollectionElement)base[ConfigurationStrings.WS2007HttpBindingCollectionElementName]; }
}
[ConfigurationProperty(ConfigurationStrings.WSDualHttpBindingCollectionElementName, Options = ConfigurationPropertyOptions.None)]
public WSDualHttpBindingCollectionElement WSDualHttpBinding
{
get { return (WSDualHttpBindingCollectionElement)base[ConfigurationStrings.WSDualHttpBindingCollectionElementName]; }
}
public static BindingsSection GetSection(Configuration config)
{
if (config == null)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("config");
}
return (BindingsSection)config.GetSection(ConfigurationStrings.BindingsSectionGroupPath);
}
public List<bindingcollectionelement> BindingCollections
{
get
{
List<bindingcollectionelement> bindingCollections = new List<bindingcollectionelement>();
foreach (ConfigurationProperty property in this.Properties)
{
bindingCollections.Add(this[property.Name]);
}
return bindingCollections;
}
}
internal static bool TryAdd(string name, Binding binding, Configuration config, out string bindingSectionName)
{
bool retval = false;
BindingsSection.Configuration = config;
try
{
retval = BindingsSection.TryAdd(name, binding, out bindingSectionName);
}
finally
{
BindingsSection.Configuration = null;
}
return retval;
}
internal static bool TryAdd(string name, Binding binding, out string bindingSectionName)
{
// TryAdd built on assumption that BindingsSectionGroup.Configuration is valid.
// This should be protected at the callers site. If assumption is invalid, then
// configuration system is in an indeterminate state. Need to stop in a manner that
// user code can not capture.
if (null == BindingsSection.Configuration)
{
DiagnosticUtility.DebugAssert("The TryAdd(string name, Binding binding, Configuration config, out string binding) variant of this function should always be called first. The Configuration object is not set.");
DiagnosticUtility.FailFast("The TryAdd(string name, Binding binding, Configuration config, out string binding) variant of this function should always be called first. The Configuration object is not set.");
}
bool retval = false;
string outBindingSectionName = null;
BindingsSection sectionGroup = BindingsSection.GetSection(BindingsSection.Configuration);
sectionGroup.UpdateBindingSections();
foreach (string sectionName in sectionGroup.BindingCollectionElements.Keys)
{
BindingCollectionElement bindingCollectionElement = sectionGroup.BindingCollectionElements[sectionName];
// Save the custom bindings as the last choice
if (!(bindingCollectionElement is CustomBindingCollectionElement))
{
MethodInfo tryAddMethod = bindingCollectionElement.GetType().GetMethod("TryAdd", BindingFlags.Instance | BindingFlags.NonPublic);
if (tryAddMethod != null)
{
retval = (bool)tryAddMethod.Invoke(bindingCollectionElement, new object[] { name, binding, BindingsSection.Configuration });
if (retval)
{
outBindingSectionName = sectionName;
break;
}
}
}
}
if (!retval)
{
// Much of the time, the custombinding should come out ok.
CustomBindingCollectionElement customBindingSection = CustomBindingCollectionElement.GetBindingCollectionElement();
retval = customBindingSection.TryAdd(name, binding, BindingsSection.Configuration);
if (retval)
{
outBindingSectionName = ConfigurationStrings.CustomBindingCollectionElementName;
}
}
// This little oddity exists to make sure that the out param is assigned to before the method
// exits.
bindingSectionName = outBindingSectionName;
return retval;
}
/// <securitynote>
/// Critical - uses SecurityCritical method UnsafeLookupCollection which elevates
/// Safe - does not leak config objects
/// </securitynote>
[SecurityCritical, SecurityTreatAsSafe]
void UpdateBindingSections()
{
ExtensionElementCollection bindingExtensions = ExtensionsSection.UnsafeLookupCollection(ConfigurationStrings.BindingExtensions, ConfigurationHelpers.GetEvaluationContext(this));
// Extension collections are additive only (BasicMap) and do not allow for <clear>
// or <remove> tags, nor do they allow for overriding an entry. This allows us
// to optimize this to only walk the binding extension collection if the counts
// mismatch.
if (bindingExtensions.Count != this.properties.Count)
{
foreach (ExtensionElement bindingExtension in bindingExtensions)
{
if (null != bindingExtension)
{
if (!this.properties.Contains(bindingExtension.Name))
{
ConfigurationProperty property = new ConfigurationProperty(bindingExtension.Name,
Type.GetType(bindingExtension.Type, true),
null,
ConfigurationPropertyOptions.None);
this.properties.Add(property);
}
}
}
}
}
/// <securitynote>
/// Critical - uses SecurityCritical method UnsafeGetAssociatedBindingCollectionElement which elevates
/// Safe - does not leak config objects
/// </securitynote>
[SecurityCritical, SecurityTreatAsSafe]
internal static void ValidateBindingReference(string binding, string bindingConfiguration, ContextInformation evaluationContext, ConfigurationElement configurationElement)
{
// ValidateBindingReference built on assumption that evaluationContext is valid.
// This should be protected at the callers site. If assumption is invalid, then
// configuration system is in an indeterminate state. Need to stop in a manner that
// user code can not capture.
if (null == evaluationContext)
{
DiagnosticUtility.DebugAssert("ValidateBindingReference() should only called with valid ContextInformation");
DiagnosticUtility.FailFast("ValidateBindingReference() should only called with valid ContextInformation");
}
if (!String.IsNullOrEmpty(binding))
{
BindingCollectionElement bindingCollectionElement = null;
if (null != evaluationContext)
{
bindingCollectionElement = ConfigurationHelpers.UnsafeGetAssociatedBindingCollectionElement(evaluationContext, binding);
}
else
{
bindingCollectionElement = ConfigurationHelpers.UnsafeGetBindingCollectionElement(binding);
}
if (bindingCollectionElement == null)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ConfigurationErrorsException(SR.GetString(SR.ConfigInvalidSection,
ConfigurationHelpers.GetBindingsSectionPath(binding)),
configurationElement.ElementInformation.Source,
configurationElement.ElementInformation.LineNumber));
}
if (!String.IsNullOrEmpty(bindingConfiguration))
{
if (!bindingCollectionElement.ContainsKey(bindingConfiguration))
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ConfigurationErrorsException(SR.GetString(SR.ConfigInvalidBindingName,
bindingConfiguration,
ConfigurationHelpers.GetBindingsSectionPath(binding),
ConfigurationStrings.BindingConfiguration),
configurationElement.ElementInformation.Source,
configurationElement.ElementInformation.LineNumber));
}
}
}
}
ContextInformation IConfigurationContextProviderInternal.GetEvaluationContext()
{
return this.EvaluationContext;
}
/// <securitynote>
/// RequiresReview - the return value will be used for a security decision -- see comment in interface definition
/// </securitynote>
ContextInformation IConfigurationContextProviderInternal.GetOriginalEvaluationContext()
{
DiagnosticUtility.DebugAssert("Not implemented: IConfigurationContextProviderInternal.GetOriginalEvaluationContext");
return null;
}
}
}
</remove></clear></bindingcollectionelement></bindingcollectionelement></bindingcollectionelement></string,></string,></string,>
This question is unlikely to help any future visitors; it is only relevant to a small geographic area, a specific moment in time, or an extraordinarily narrow situation that is not generally applicable to the worldwide audience of the internet. For help making this question more broadly applicable, visit the help center.
Closed 10 years ago.
I want to get opinions on the code I have put together for a centralized DataContext via a DataHelper class that I have created to be re-used on projects.
NOTE - there is a ton of code here, sorry about that, but I really wanted to layout out the complete approach and uses for my ideas. I'm an not saying this is the right approach, but it works for me so far (still playing with the approach, nothing in production yet, but very similar to stuff I have built over the years) and I really want to get constructive feedback from the community on what I have built to see if it is insane, great, can be improved, etc...
A few thoughts I put into this:
Data Context needs to be stored in a common memory space, easily accessible
Transactions should take the same approach
It must be disposed of properly
Allows for better separation of business logic for Saving and Deleting in transactions.
Here is the code for each item:
1 - First the data context stored in either the current HttpContext.Current.Items collection (so it only lives for the life of the page and only is fired up once at the first requested) or if the HttpContext doesn't exist uses a ThreadSlot (in which case that code most clean it up itself, like a console app using it...):
public static class DataHelper {
/// <summary>
/// Current Data Context object in the HTTPContext or Current Thread
/// </summary>
public static TemplateProjectContext Context {
get {
TemplateProjectContext context = null;
if (HttpContext.Current == null) {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("DataHelper.CurrentContext");
if (Thread.GetData(threadSlot) == null) {
context = new TemplateProjectContext();
Thread.SetData(threadSlot, context);
} else {
context = (TemplateProjectContext)Thread.GetData(threadSlot);
}
} else {
if (HttpContext.Current.Items["DataHelper.CurrentContext"] == null) {
context = new TemplateProjectContext();
HttpContext.Current.Items["DataHelper.CurrentContext"] = context;
} else {
context = (TemplateProjectContext)HttpContext.Current.Items["DataHelper.CurrentContext"];
}
}
return context;
}
set {
if (HttpContext.Current == null) {
if (value == null) {
Thread.FreeNamedDataSlot("DataHelper.CurrentContext");
} else {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("DataHelper.CurrentContext");
Thread.SetData(threadSlot, value);
}
} else {
if (value == null)
HttpContext.Current.Items.Remove("DataHelper.CurrentContext");
else
HttpContext.Current.Items["DataHelper.CurrentContext"] = value;
}
}
}
...
2 - To support transactions, I use a similar approach, and also include helper methods to Begin, Commit and Rollback:
/// <summary>
/// Current Transaction object in the HTTPContext or Current Thread
/// </summary>
public static DbTransaction Transaction {
get {
if (HttpContext.Current == null) {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("currentTransaction");
if (Thread.GetData(threadSlot) == null) {
return null;
} else {
return (DbTransaction)Thread.GetData(threadSlot);
}
} else {
if (HttpContext.Current.Items["currentTransaction"] == null) {
return null;
} else {
return (DbTransaction)HttpContext.Current.Items["currentTransaction"];
}
}
}
set {
if (HttpContext.Current == null) {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("currentTransaction");
Thread.SetData(threadSlot, value);
} else {
HttpContext.Current.Items["currentTransaction"] = value;
}
}
}
/// <summary>
/// Begins a transaction based on the common connection and transaction
/// </summary>
public static void BeginTransaction() {
DataHelper.Transaction = DataHelper.CreateSqlTransaction();
}
/// <summary>
/// Creates a SqlTransaction object based on the current common connection
/// </summary>
/// <returns>A new SqlTransaction object for the current common connection</returns>
public static DbTransaction CreateSqlTransaction() {
return CreateSqlTransaction(DataHelper.Context.Connection);
}
/// <summary>
/// Creates a SqlTransaction object for the requested connection object
/// </summary>
/// <param name="connection">Reference to the connection object the transaction should be created for</param>
/// <returns>New transaction object for the requested connection</returns>
public static DbTransaction CreateSqlTransaction(DbConnection connection) {
if (connection.State != ConnectionState.Open) connection.Open();
return connection.BeginTransaction();
}
/// <summary>
/// Rolls back and cleans up the current common transaction
/// </summary>
public static void RollbackTransaction() {
if (DataHelper.Transaction != null) {
DataHelper.RollbackTransaction(DataHelper.Transaction);
if (HttpContext.Current == null) {
Thread.FreeNamedDataSlot("currentTransaction");
} else {
HttpContext.Current.Items.Remove("currentTransaction");
}
}
}
/// <summary>
/// Rolls back and disposes of the requested transaction
/// </summary>
/// <param name="transaction">The transaction to rollback</param>
public static void RollbackTransaction(DbTransaction transaction) {
transaction.Rollback();
transaction.Dispose();
}
/// <summary>
/// Commits and cleans up the current common transaction
/// </summary>
public static void CommitTransaction() {
if (DataHelper.Transaction != null) {
DataHelper.CommitTransaction(DataHelper.Transaction);
if (HttpContext.Current == null) {
Thread.FreeNamedDataSlot("currentTransaction");
} else {
HttpContext.Current.Items.Remove("currentTransaction");
}
}
}
/// <summary>
/// Commits and disposes of the requested transaction
/// </summary>
/// <param name="transaction">The transaction to commit</param>
public static void CommitTransaction(DbTransaction transaction) {
transaction.Commit();
transaction.Dispose();
}
3 - Clean and easy Disposal
/// <summary>
/// Cleans up the currently active connection
/// </summary>
public static void Dispose() {
if (HttpContext.Current == null) {
LocalDataStoreSlot threadSlot = Thread.GetNamedDataSlot("DataHelper.CurrentContext");
if (Thread.GetData(threadSlot) != null) {
DbTransaction transaction = DataHelper.Transaction;
if (transaction != null) {
DataHelper.CommitTransaction(transaction);
Thread.FreeNamedDataSlot("currentTransaction");
}
((TemplateProjectContext)Thread.GetData(threadSlot)).Dispose();
Thread.FreeNamedDataSlot("DataHelper.CurrentContext");
}
} else {
if (HttpContext.Current.Items["DataHelper.CurrentContext"] != null) {
DbTransaction transaction = DataHelper.Transaction;
if (transaction != null) {
DataHelper.CommitTransaction(transaction);
HttpContext.Current.Items.Remove("currentTransaction");
}
((TemplateProjectContext)HttpContext.Current.Items["DataHelper.CurrentContext"]).Dispose();
HttpContext.Current.Items.Remove("DataHelper.CurrentContext");
}
}
}
3b - I'm building this in MVC, so I have a "base" Controller class that all my controllers inherit from - this way the Context only lives from when first accessed on a request, and until the page is disposed, that way its not too "long running"
using System.Web.Mvc;
using Core.ClassLibrary;
using TemplateProject.Business;
using TemplateProject.ClassLibrary;
namespace TemplateProject.Web.Mvc {
public class SiteController : Controller {
protected override void Dispose(bool disposing) {
DataHelper.Dispose();
base.Dispose(disposing);
}
}
}
4 - So I am big on business classes, separation of concerns, reusable code, all that wonderful stuff. I have an approach that I call "Entity Generic" that can be applied to any entity in my system - for example, Addresses and Phones
A Customer can have 1 or more of each, along with a Store, Person, or anything really - so why add street, city, state, etc to every thing that needs it when you can just build an Address entity, that takes a Foreign Type and Key (what I call EntityType and EntityId) - then you have a re-usable business object, supporting UI control, etc - so you build it once and re-use it everywhere.
This is where the centralized approach I am pushing for here really comes in handy and I think makes the code much cleaner than having to pass the current data context/transaction into every method.
Take for example that you have a Page for a customer, the Model includes the Customer data, Contact, Address and a few Phone Numbers (main, fax, or cell, whatever)
When getting a Customer Edit Model for the page, here is a bit of the code I have put together (see how I use the DataHelper.Context in the LINQ):
public static CustomerEditModel FetchEditModel(int customerId) {
if (customerId == 0) {
CustomerEditModel model = new CustomerEditModel();
model.MainContact = new CustomerContactEditModel();
model.MainAddress = new AddressEditModel();
model.ShippingAddress = new AddressEditModel();
model.Phone = new PhoneEditModel();
model.Cell = new PhoneEditModel();
model.Fax = new PhoneEditModel();
return model;
} else {
var output = (from c in DataHelper.Context.Customers
where c.CustomerId == customerId
select new CustomerEditModel {
CustomerId = c.CustomerId,
CompanyName = c.CompanyName
}).SingleOrDefault();
if (output != null) {
output.MainContact = CustomerContact.FetchEditModelByPrimary(customerId) ?? new CustomerContactEditModel();
output.MainAddress = Address.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, AddressTypes.Main) ?? new AddressEditModel();
output.ShippingAddress = Address.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, AddressTypes.Shipping) ?? new AddressEditModel();
output.Phone = Phone.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, PhoneTypes.Main) ?? new PhoneEditModel();
output.Cell = Phone.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, PhoneTypes.Cell) ?? new PhoneEditModel();
output.Fax = Phone.FetchEditModelByType(BusinessEntityTypes.Customer, customerId, PhoneTypes.Fax) ?? new PhoneEditModel();
}
return output;
}
}
And here is a sample of the the phone returning the Edit model to be used:
public static PhoneEditModel FetchEditModelByType(byte entityType, int entityId, byte phoneType) {
return (from p in DataHelper.Context.Phones
where p.EntityType == entityType
&& p.EntityId == entityId
&& p.PhoneType == phoneType
select new PhoneEditModel {
PhoneId = p.PhoneId,
PhoneNumber = p.PhoneNumber,
Extension = p.Extension
}).FirstOrDefault();
}
Now the page has posted back and this all needs to be save, so the Save method in my control just lets the business object handle this all:
[Authorize(Roles = SiteRoles.SiteAdministrator + ", " + SiteRoles.Customers_Edit)]
[HttpPost]
public ActionResult Create(CustomerEditModel customer) {
return CreateOrEdit(customer);
}
[Authorize(Roles = SiteRoles.SiteAdministrator + ", " + SiteRoles.Customers_Edit)]
[HttpPost]
public ActionResult Edit(CustomerEditModel customer) {
return CreateOrEdit(customer);
}
private ActionResult CreateOrEdit(CustomerEditModel customer) {
if (ModelState.IsValid) {
SaveResult result = Customer.SaveEditModel(customer);
if (result.Success) {
return RedirectToAction("Index");
} else {
foreach (KeyValuePair<string, string> error in result.ErrorMessages) ModelState.AddModelError(error.Key, error.Value);
}
}
return View(customer);
}
And inside the Customer business object - it handles the transaction centrally and lets the Contact, Address and Phone business classes do their thing and really not worry about the transaction:
public static SaveResult SaveEditModel(CustomerEditModel model) {
SaveResult output = new SaveResult();
DataHelper.BeginTransaction();
try {
Customer customer = null;
if (model.CustomerId == 0) customer = new Customer();
else customer = DataHelper.Context.Customers.Single(c => c.CustomerId == model.CustomerId);
if (customer == null) {
output.Success = false;
output.ErrorMessages.Add("CustomerNotFound", "Unable to find the requested Customer record to update");
} else {
customer.CompanyName = model.CompanyName;
if (model.CustomerId == 0) {
customer.SiteGroup = CoreSession.CoreSettings.CurrentSiteGroup;
customer.CreatedDate = DateTime.Now;
customer.CreatedBy = SiteLogin.Session.ActiveUser;
DataHelper.Context.Customers.AddObject(customer);
} else {
customer.ModifiedDate = DateTime.Now;
customer.ModifiedBy = SiteLogin.Session.ActiveUser;
}
DataHelper.Context.SaveChanges();
SaveResult result = Address.SaveEditModel(model.MainAddress, BusinessEntityTypes.Customer, customer.CustomerId, AddressTypes.Main, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = Address.SaveEditModel(model.ShippingAddress, BusinessEntityTypes.Customer, customer.CustomerId, AddressTypes.Shipping, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = Phone.SaveEditModel(model.Phone, BusinessEntityTypes.Customer, customer.CustomerId, PhoneTypes.Main, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = Phone.SaveEditModel(model.Fax, BusinessEntityTypes.Customer, customer.CustomerId, PhoneTypes.Fax, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = Phone.SaveEditModel(model.Cell, BusinessEntityTypes.Customer, customer.CustomerId, PhoneTypes.Cell, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
result = CustomerContact.SaveEditModel(model.MainContact, customer.CustomerId, false);
if (!result.Success) {
output.Success = false;
output.ErrorMessages.Concat(result.ErrorMessages);
}
if (output.Success) {
DataHelper.Context.SaveChanges();
DataHelper.CommitTransaction();
} else {
DataHelper.RollbackTransaction();
}
}
} catch (Exception exp) {
DataHelper.RollbackTransaction();
ErrorHandler.Handle(exp, true);
output.Success = false;
output.ErrorMessages.Add(exp.GetType().ToString(), exp.Message);
output.Exceptions.Add(exp);
}
return output;
}
Notice how each Address, Phone, Etc is handled by its own business class, here is the Phone's save method - notice how it doesn't actually do the save here unless you tell it to (save is handled in the Customer's method so save is called just once for the context)
public static SaveResult SaveEditModel(PhoneEditModel model, byte entityType, int entityId, byte phoneType, bool saveChanges) {
SaveResult output = new SaveResult();
try {
if (model != null) {
Phone phone = null;
if (model.PhoneId != 0) {
phone = DataHelper.Context.Phones.Single(x => x.PhoneId == model.PhoneId);
if (phone == null) {
output.Success = false;
output.ErrorMessages.Add("PhoneNotFound", "Unable to find the requested Phone record to update");
}
}
if (string.IsNullOrEmpty(model.PhoneNumber)) {
if (model.PhoneId != 0 && phone != null) {
DataHelper.Context.Phones.DeleteObject(phone);
if (saveChanges) DataHelper.Context.SaveChanges();
}
} else {
if (model.PhoneId == 0) phone = new Phone();
if (phone != null) {
phone.EntityType = entityType;
phone.EntityId = entityId;
phone.PhoneType = phoneType;
phone.PhoneNumber = model.PhoneNumber;
phone.Extension = model.Extension;
if (model.PhoneId == 0) {
phone.CreatedDate = DateTime.Now;
phone.CreatedBy = SiteLogin.Session.ActiveUser;
DataHelper.Context.Phones.AddObject(phone);
} else {
phone.ModifiedDate = DateTime.Now;
phone.ModifiedBy = SiteLogin.Session.ActiveUser;
}
if (saveChanges) DataHelper.Context.SaveChanges();
}
}
}
} catch (Exception exp) {
ErrorHandler.Handle(exp, true);
output.Success = false;
output.ErrorMessages.Add(exp.GetType().ToString(), exp.Message);
output.Exceptions.Add(exp);
}
return output;
}
FYI - SaveResult is just a little container class that I used to get detailed information back if a save fails:
public class SaveResult {
private bool _success = true;
public bool Success {
get { return _success; }
set { _success = value; }
}
private Dictionary<string, string> _errorMessages = new Dictionary<string, string>();
public Dictionary<string, string> ErrorMessages {
get { return _errorMessages; }
set { _errorMessages = value; }
}
private List<Exception> _exceptions = new List<Exception>();
public List<Exception> Exceptions {
get { return _exceptions; }
set { _exceptions = value; }
}
}
The other piece to this is the re-usable UI code for the Phone, Address, etc - which handles all the validation, etc in just one location too.
So, let your thoughts flow and thanks for taking the time to read/review this huge post!