MVC Cache VaryByHeader, need to remove cache by header also - c#

I have a MVC site that I am developing that is multi-tenant application. I have set up the cache to varybyheader="host". Now I'd like to invalidate the cache only by hostname.
The RemoveOutputCacheItem only takes absolute virtual paths and isn't allowing a custom host name (there by being a non-virtual path).
Any help on how to achieve this?
Thanks.
Update
Here is how I can get the internal cache keys
var runtimeType = typeof(HttpRuntime);
var ci = runtimeType.GetProperty(
"CacheInternal",
BindingFlags.NonPublic | BindingFlags.Static);
var cache = ci.GetValue(ci, new object[0]);
var cachesInfo = cache.GetType().GetField(
"_caches",
BindingFlags.NonPublic | BindingFlags.Instance);
var cacheEntries = cachesInfo.GetValue(cache);
var outputCacheEntries = new List<object>();
foreach (Object singleCache in cacheEntries as Array)
{
var singleCacheInfo =
singleCache.GetType().GetField("_entries",
BindingFlags.NonPublic | BindingFlags.Instance);
var entries = singleCacheInfo.GetValue(singleCache);
foreach (DictionaryEntry cacheEntry in entries as Hashtable)
{
var cacheEntryInfo = cacheEntry.Value.GetType().GetField("_value",
BindingFlags.NonPublic | BindingFlags.Instance);
var value = cacheEntryInfo.GetValue(cacheEntry.Value);
if (value.GetType().Name == "CachedRawResponse")
{
var key = (string)cacheEntry.Value.GetType().BaseType.GetField("_key", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(cacheEntry.Value);
key = key.Substring(key.IndexOf("/"));
outputCacheEntries.Add(key);
}
}
}
var keys = new StringBuilder();
foreach (string key in outputCacheEntries)
{
if (key.Contains(Request.Url.Host))
{
keys.Append(key + " ");
HttpResponse.RemoveOutputCacheItem(key);
}
}
That RemoveOutputCacheItem doesn't work with this key.
They key is generated like this: /HQNnoneV+n+FCNhostVHOSTNAME.comDE
Even a direct call RemoveOutputCache("/HOSTNAME.com") doesn't work either (with vary by custom).
Update #2
So read through the reference source code (http://referencesource.microsoft.com/#System.Web/HttpResponse.cs,3222f830c91ccb06) and it appears that it should attempt to create the custom key. So I should be able to RemoveOutputCache("/") and it should create the custom key for me, but this also appears to not be working as expected, it still appears to clear all keys.

You can try the VaryByCustom
[OutputCache(Duration = 3600, VaryByCustom = "hostname")]
And define the VaryByCustom like:
public override string GetVaryByCustomString(HttpContext Context, string Custom)
{
//Here you set any cache invalidation policy
if (Custom == "hostname")
{
return Context.Request.Url.Host;
}
return base.GetVaryByCustomString(Context, Custom);
}

The answer is that this cannot be done in this manner. Digging into the actual code for RemoveOutputCache where it generates the key doesn't pass in the varyby parameter and therefore I am unable to get the specific key needed.
Perhaps if I dug a bit more into the key generation, perhaps I could write a key gen method, but I've gone a different route. This is just to give everyone else an answer to my issue.
Again I only wanted to vary by header, no params and be able to invalidate just that one cached item.

I'm a little late to the party, but I had the exact same need, and ended up being inspired by the answers in this SO question: Clearing Page Cache in ASP.NET
I setup the following below test, and was able to successfully clear the cache per host with no additional code...
[OutputCache(VaryByHeader = "host", Duration = 600, Location = OutputCacheLocation.Server)]
public ActionResult TestPage()
{
var key = GetCacheKey("TestPage");
HttpContext.Cache[key] = new object();
Response.AddCacheItemDependency(key);
return Content(DateTime.Now.ToString());
}
public ActionResult TestClearCache()
{
var key = GetCacheKey("TestPage");
HttpContext.Cache.Remove(key);
return Content("Cache cleared");
}
private string GetCacheKey(string page)
{
return string.Concat(page, Request.Url.Host.ToLower());
}

Related

Sharepoint 2013 On-Premises C# CSOM Cross Site Collection List Access

I see this has sorta been asked a bunch times before, but all the examples I am running across will not work or are in JavaScript , I NEED HELP WITH C# please.
I have a farm with server site collections on it, I successfully created a provider hosted addin/App, When it trys to access lists on the Web that launched it everything is fine! I need to try to access lists on other webs in the same farm and on the same domain does anyone have an example of C# code that can do this
You can create a repository method like this:
public class SharepointRepository
{
public ListItemCollection ListTopN(string urlSite, string listName, bool ascending, string column, int rowLimit)
{
using (var context = new ClientContext(urlSite))
{
context.Credentials = CredentialCache.DefaultCredentials;
List list = context.Web.Lists.GetByTitle(listName);
string myQuery = string.Format("<View><Query><OrderBy><FieldRef Name='{0}' Ascending='{1}' /></OrderBy></Query><RowLimit>{2}</RowLimit></View>", column, ascending.ToString(), rowLimit);
CamlQuery query = new CamlQuery();
query.ViewXml = myQuery;
ListItemCollection collection = list.GetItems(query);
context.Load(list);
context.Load(collection);
context.ExecuteQuery();
return collection;
}
}
}
this approach uses the managed csom.
and if you are facing problems with ADFS, try adding after this line
context.Credentials = CredentialCache.DefaultCredentials;
this
context.ExecutingWebRequest += new EventHandler<WebRequestEventArgs>(MixedAuthRequestMethod);
and this function
void MixedAuthRequestMethod(object sender, WebRequestEventArgs e)
{
e.WebRequestExecutor.RequestHeaders.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f");
}
This is a basic referente:
https://msdn.microsoft.com/en-us/library/office/fp179912.aspx
You should also look at the Sharepoint App Model and the Rest OData API.
I figured this out, so I am posting it just in case someone else needs it, also please understand that I am total noob to SharePoint and this may not be the best way or even the SharePoint Accepted way of doing things.
First you need to give you app Tenant Permission (Full Control or Manage) ! - Very Important
Second I created this function that make a n SharePoint Context to a site other then the one the app is running on
public ClientContext CreateRemoteSharePointContext(string TargetWebURL, SharePointContext CurrentSharePointContext)
{
//In order for us to create a share point client context that points to
//site other then the site that this app is running we need to copy some url parameters from the current
//context. These parameters are found on the current share-point context
NameValueCollection QueryString = Request.QueryString;
//Since, The Query string is a read only collection, a use of reflection is required to update the
//values on the request object, we must use the current request object because it contains
//security and other headers/cookies that we need for the context to be created, Grab the url params that we need
//other then TargetWebUrl, that will be the url of the site we want to manipulate
Utility.AddToReadonlyQueryString(QueryString, "SPHostUrl", CurrentSharePointContext.SPHostUrl.ToString(), System.Web.HttpContext.Current.Request);
Utility.AddToReadonlyQueryString(QueryString, "SPAppWebUrl", TargetWebURL, System.Web.HttpContext.Current.Request);
Utility.AddToReadonlyQueryString(QueryString, "SPLanguage", CurrentSharePointContext.SPLanguage, System.Web.HttpContext.Current.Request);
Utility.AddToReadonlyQueryString(QueryString, "SPClientTag", CurrentSharePointContext.SPClientTag, System.Web.HttpContext.Current.Request);
Utility.AddToReadonlyQueryString(QueryString, "SPProductNumber", CurrentSharePointContext.SPProductNumber, System.Web.HttpContext.Current.Request);
//This is a special line, we need to get the AppOnly access token and pass it along to the target site, its is a little counter intuitive
//Because we are using TokenHelper.GetS2SAccessToeknWithWindowsIdentity - but we pass NULL as the User identity, this will
//check the app manifest and if the app has a CERT and AppOnly Permission it will return a valid app only token to use
Utility.AddToReadonlyQueryString(QueryString, "AppContextToken", TokenHelper.GetS2SAccessTokenWithWindowsIdentity(new Uri(TargetWebURL), null), System.Web.HttpContext.Current.Request);
//Return the newly created context
return SharePointContextProvider.Current.CreateSharePointContext(HttpContext.Request, TargetWebURL).CreateAppOnlyClientContextForSPAppWeb();
}
As you can see the I had to kinda hack up the Querystring and grab some values so here is the Utility class that does that :
public class Utility
{
public static void UpdateReadonlyQueryString(NameValueCollection collectionToUpdate, string paramName, string paramValue, HttpRequest Request)
{
collectionToUpdate = (NameValueCollection)Request.GetType().GetField("_queryString", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(Request);
PropertyInfo readOnlyInfo = collectionToUpdate.GetType().GetProperty("IsReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
readOnlyInfo.SetValue(collectionToUpdate, false, null);
collectionToUpdate[paramName] = paramValue;
readOnlyInfo.SetValue(collectionToUpdate, true, null);
}
public static void AddToReadonlyQueryString(NameValueCollection collectionToUpdate, string paramName, string paramValue, HttpRequest Request)
{
collectionToUpdate = Request.QueryString;
collectionToUpdate = (NameValueCollection)Request.GetType().GetField("_queryString", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(Request);
PropertyInfo readOnlyInfo = collectionToUpdate.GetType().GetProperty("IsReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
readOnlyInfo.SetValue(collectionToUpdate, false, null);
collectionToUpdate.Add( paramName, paramValue);
readOnlyInfo.SetValue(collectionToUpdate, true, null);
}
}
and Finally the SharePoint access code the looks like much of the same SharePoint code out there on the web, I had to remove some stuff from it that would identify the project or who its for, but it should be easy to pick out what you need from in side
try
{
//Get the name of the sharepoint list that needs to be updated from settings
var ListName = ConfigurationManager.AppSettings[Constants.SettingsConstants.SPLaunchQueList];
var TargetSiteToUpdate = "URL TO THE SITE YOUR TRYING TO UPDATE";
//Get the sharepoint context from session
var spContext = <SOME HOW CREATE YOUR CONTEXT>
//Lets create a client context from the current sharepoint context to the target site
//NOTE this requires the application to HAVE Tenant level permission, it must be trusted by
//the farm admin
using (var spClientContext = CreateRemoteSharePointContext(TargetSiteToUpdate, spContext))
{
//Get the current Web (Sharepoint Web) from the client context
var web = spClientContext.Web;
//Load all the webs properties including title , url all the lists and get the subwebs if any as well
spClientContext.Load(web, x => x.Title, x => x.Url, x => x.Lists, x => x.Webs.Include(w => w.Title, w => w.Url));
spClientContext.ExecuteQuery();
//Lets grab the list that needs to be updated
SP.List OrgList = web.Lists.GetByTitle(ListName);
//Construct a caml query Where the groupID of the SQL Server record is the same
//as the list GroupID
var caml = new Caml<DetailParts>().Where(o => o.GroupID == updateRecord.GroupID);
CamlQuery camlQuery = new CamlQuery();
camlQuery.ViewXml = caml.ToString();
//Load the CAML query
ListItemCollection Rows = OrgList.GetItems(camlQuery);
spClientContext.Load(Rows);
spClientContext.ExecuteQuery();
//The CAML Query should only return one row because GroupID should be UNQIUE
//however due to how sharepoint returns list data we are forcing the first value
//here
ListItem RowToUpdate = Rows[0];
//Get a list of sharepoint columns that match the local detail parts
var ColumnsToUpdate = GetSharePointColumns(typeof(DetailParts));
RowToUpDate["SomeColumn"] = "YOUR NEW VALUE";
RowToUpdate.Update();
//Commit the changes
spClientContext.ExecuteQuery();
}
}
}
catch (Exception ex)
{
//Log any exception and then throw to the caller
logger.Error("Sharepoint exception", ex);
}
That last section of code should be in a function or method of some sort I just pull out the relevant parts. As I Stated this is the only way I found that works and if someone has a better way please share it as I am not a SharePoint expert.

How to prevent fill in values when saving over CSOM to a choice field

I am writing some ETL code to move data between an external system and SharePoint Online.
I am using the nuget package Microsoft.SharePointOnline.CSOM to communicate with SP in C#.
I am using the following code to update my field values.
spListItem[fieldName] = "Test Value";
spListItem.Update();
spClientContext.ExecuteQuery();
I noticed with Choice fields, if I save a non existing value SharePoint does not complain and just adds the value even if Allow 'Fill-in' choices is set to NO.
Is there a validate function anywhere in SharePoint? I saw some methods like ValidateUpdateListItem, but they didn't seem to do what I needed.
You could consider to validate choice field value before saving its value as demonstrated below:
static class ListItemExtensions
{
public static bool TryValidateAndUpdateChoiceFieldValue(this ListItem item, string fieldName, string fieldValue)
{
var ctx = item.Context;
var field = item.ParentList.Fields.GetByInternalNameOrTitle(fieldName);
ctx.Load(field);
ctx.ExecuteQuery();
var choiceField = ctx.CastTo<FieldChoice>(field);
if (!choiceField.FillInChoice)
{
var allowedValues = choiceField.Choices;
if (!allowedValues.Contains(fieldValue))
{
return false;
}
}
item.Update();
return true;
}
}
In that case the ListItem will be updated once the validation is
succeeded.
Usage
using (var ctx = new ClientContext(webUri))
{
var list = ctx.Web.Lists.GetByTitle(listTitle);
var listItem = list.GetItemById(itemId);
if(listItem.TryValidateAndUpdateChoiceFieldValue(fieldName,fieldValue))
ctx.ExecuteQuery();
}

Put SecureString into PasswordBox

I have an existing SecureString that I would like to put into a PasswordBox without revealing the .Password. Can this be done? For example:
tbPassword.SecurePassword = DecryptString(Properties.Settings.Default.proxyPassword);
In this case DecryptString produces a SecureString. However, SecurePassword is a read-only property so I can't assign a value to it.
You can't.
However, what you can do is put placeholder text in it's place (it can even be "placeholder", we are only using it to make a few dots to show up in the box).
After you put the placeholder in, when you go to retrieve the "current password" somewhere in your program first check if the PasswordChanged event has fired since you entered the placeholder password. If the event has not fired use the old stored password, if the event has fired use the current password from the SecurePassword property of PasswordBox.
Sorry for the late addition, but it might be an idea for people who also walk into this.
(At least I ended up on this page in 2021 looking for it)
Looking at the source of PasswordBox, we can see how its properties are implemented. The Password property setter just copies the String into a temporary SecureString and forwards it to its internal storage.
The readonly SecurePassword property returns a copy of the internal SecureString, so calling .Clear() / .AppendChar(char) on it will only change this copy, if .MakeReadonly() has not been called on it.
[TemplatePart(Name = "PART_ContentHost", Type = typeof (FrameworkElement))]
public sealed class PasswordBox : Control, ITextBoxViewHost
{
public SecureString SecurePassword => this.TextContainer.GetPasswordCopy();
[DefaultValue("")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public unsafe string Password
{
[SecurityCritical] get { /* left out to reduce space */ }
[SecurityCritical] set
{
if (value == null)
value = string.Empty;
// We want to replicate this, but copy a SecureString instead of creating one from a String
using (SecureString secureString = new SecureString())
{
for (int index = 0; index < value.Length; ++index)
secureString.AppendChar(value[index]);
this.SetSecurePassword(secureString);
}
}
}
}
It may be a bit hacky, but calling the private SetSecurePassword may be closest to bypassing conversion to clear text in order to use the Password setter : (we make a temporary copy just like in the .Password setter as we are not responsible for managing lifetime of the provided SecureString, which could even be readonly)
// option 1: streight reflection
var setPasswordMethod = typeof(PasswordBox).GetMethod("SetSecurePassword", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] {typeof(SecureString)}, null);
using (var copy = mySecurePassword.Copy())
setPasswordMethod.Invoke(PasswordControl, new[] {copy});
// option 2: compiled delegate so reflection will only kick in once
Action<PasswordBox, SecureString> setSecurePassword = null; // this would be a cache lookup instead of a local variable.
if (setSecurePassword == null)
{
var passwordBox = Expression.Parameter(typeof(PasswordBox), "passwordBox");
var password = Expression.Parameter(typeof(SecureString), "securePassword");
//// if we want to include code for making the temporary copy in the delegate, use this instead to create its body
//var passwordCopy = Expression.Variable(typeof(SecureString));
//var makePasswordCopy = Expression.Call(password, nameof(SecureString.Copy), Type.EmptyTypes);
//var body = Expression.Block(new[] {passwordCopy},
// Expression.Assign(passwordCopy, makePasswordCopy),
// Expression.TryFinally(
// Expression.Call(passwordBox, "SetSecurePassword", Type.EmptyTypes, passwordCopy),
// Expression.Call(Expression.Convert(passwordCopy, typeof(IDisposable)),
// nameof(IDisposable.Dispose), Type.EmptyTypes)));
var body = Expression.Call(passwordBox, "SetSecurePassword", Type.EmptyTypes, password);
setSecurePassword = Expression.Lambda<Action<PasswordBox, SecureString>>(body, passwordBox, password).Compile();
}
using (var copy = mySecurePassword.Copy()) // if we would make the copy inside the delegate, we won't need to do it here.
setSecurePassword(PasswordControl, copy);
I hope this still helps anyone.

Display sharepoint people/group field list's value in people editor

i want to display value of sharepoint people/group value in people editor(web part) when the page is loaded. This is the code that i use to get the value displayed in web part
if(SPContext .Current .ListItem .ID >= 1)
using (SPSite site = new SPSite("sitename"))
{
using (SPWeb web = site.OpenWeb())
{
var id = SPContext.Current.ListItem.ID;
SPList lists = web.Lists["DDClist"];
SPListItem item = lists.GetItemById(id);
{
string test = Convert.ToString(item["Project No"]);
tb_pno.Text = test;
string test2 = Convert.ToString(item["Project Title"]);
tb_pname.Text = test2;
string test3 = Convert.ToString(item["DDC No"]);
tb_idcno.Text = test3;
string test4 = Convert.ToString(item["Date In"]);
TextBox3.Text = test4;
}
}
}
is there a way to do the same thing with people editor?
This is all a little tricky; when I've had to do it before, I use the following to get SPUser object out of a field:
SPUser singleUser = new SPFieldUserValue(
item.Web, item["Single User"] as string).User;
SPUser[] multipleUsers = ((SPFieldUserValueCollection)item["MultipleUsers"])
.Cast<SPFieldUserValue>().Select(f => f.User);
I'm not sure why one user is stored as a string, but multiple users are stored as a specific object; it may also not be consistent in this so you might have to debug a bit and see what the type in your field is.
Once you have these SPUsers, you can populate your PeopleEditor control
using the account names as follows (quite long-winded):
ArrayList entityArrayList = new ArrayList();
foreach(SPUser user in multipleUsers) // or just once for a single user
{
PickerEntity entity = new PickerEntity;
entity.Key = user.LoginName;
entity = peMyPeople.ValidateEntity(entity);
entityArrayList.Add(entity);
}
peMyPeople.UpdateEntities(entityArrayList);
This also performs validation of the users of some kind.
If the page this control appears on may be posted-back, you need the following to be done during the postback in order for the values to be correctly roundtripped; I put it in PreRender but it could happen elsewhere in the lifecycle:
protected override void OnPreRender(EventArgs e)
{
if (IsPostBack)
{
var csa = peMyPeople.CommaSeparatedAccounts;
csa = peMyPeople.CommaSeparatedAccounts;
}
}
If you want to check any error messages that the control generates for you (if the user input is incorrect), you need to have done this switchout already:
var csa = usrBankSponsor.CommaSeparatedAccounts;
csa = usrOtherBankParties.CommaSeparatedAccounts;
//ErrorMessage is incorrect if you haven't done the above
if (!String.IsNullOrEmpty(usrBankSponsor.ErrorMessage))
{
...
}
It's really not very nice and there may be a much better way of handling it, but this is the result of my experience so far so hopefully it will save you some time.

can I use dynamic to redirect HttpContext.Current.Request?

I'm using RhinoMocks for testing. It's not good at redirecting statics; I've considered using another library like the successor to Moles (edit: I guess the Fakes tool is only available in VS2012? that stinks) or TypeMock, but would prefer not to.
I have a 3rd party library that takes in an HttpRequest object. My first stab at it was to use:
public void GetSamlResponseFromHttpPost(out XmlElement samlResponse,
out string relayState, HttpContextBase httpContext = null)
{
var wrapper = httpContext ?? new HttpContextWrapper(HttpContext.Current);
// signature of the next line cannot be changed
ServiceProvider.ReceiveSAMLResponseByHTTPPost(
wrapper.ApplicationInstance.Context.Request, out samlResponse, out relayState);
All looked fine, until I went to test it. The real issue here is that I need to stub out wrapper.ApplicationInstance.Context.Request. Which leads into a whole host of old school 'ASP.NET don't like testing' pain.
I have heard that you can use dynamic magic in C# to redirect static methods. Can't find any examples of doing this with something like HttpContext though. Is this possible?
Not an ideal solution, but my solution to test this was to use reflection and modify the object under the hood:
httpRequest = new HttpRequest("default.aspx", "http://test.com", null);
var collection = httpRequest.Form;
// inject a value into the Form directly
var propInfo = collection.GetType().GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
propInfo.SetValue(collection, false, new object[] { });
collection["theFormField"] = val;
propInfo.SetValue(collection, true, new object[] { });
var appInstance = new HttpApplication();
var w = new StringWriter();
httpResponse = new HttpResponse(w);
httpContext = new HttpContext(httpRequest, httpResponse);
// set the http context on the app instance to a new value
var contextField = appInstance.GetType().GetField("_context", BindingFlags.Instance | BindingFlags.NonPublic);
contextField.SetValue(appInstance, httpContext);
Context.Stub(ctx => ctx.ApplicationInstance).Return(appInstance);
My goal here was to have wrapper.ApplicationInstance.Context.Request return a form field value when asked. It may have been roundabout, but it works. This code only exists in test code so I'm happy with it.

Categories