RazorEngine throw a NotSupportedException when compiling template in Azure Function - azure

I must compile a razor view in an azure function to send an email, but something goes wrong. I obtain the a NotSupportedException error: The given path's format is not supported.
Here my code:
private IRazorEngineService _engine;
public MyCtor(bool isTestEnvironment)
{
TemplateServiceConfiguration configuration = new TemplateServiceConfiguration();
configuration.Debug = isTestEnvironment;
this._engine = RazorEngineService.Create(configuration);
}
public string GetHtmlEmailBody(string templateFileName, object emailData, string layoutFileName)
{
//Get data type of email data
Type emailDataType = emailData.GetType();
string layoutFullFileName = Path.Combine(this._layoutPath, layoutFileName);
string layoutContentString = File.ReadAllText(layoutFullFileName);
var layout = new LoadedTemplateSource(layoutContentString, layoutFullFileName);
this._engine.AddTemplate("layoutName", layout);
string templateFullFileName = Path.Combine(this._templatePath, templateFileName);
string templateContentString = File.ReadAllText(templateFullFileName);
var template = new LoadedTemplateSource(templateContentString, templateFullFileName);
this._engine.AddTemplate("templateName", template);
this._engine.Compile("templateName"); //<-- Here I get the exception
string htmlEmailBody = this._engine.Run("templateName", emailDataType, emailData);
return htmlEmailBody;
}
Paths are similar to D:\\...\\Emails\\Templates.. I am testing locally and it does not work... I have googled and it seems that Azure Functions have some limitations in caching and in file system management, but it is not clear how can I solve the problem.
I think I have same problem this person has written here
Any idea how can I solve it? There is something wrong in what I am doing?
I am using RazorEngine 3.10.0
Thank you

I have found the problem, downloading the code and doing reverse engineering.
Problem was inside the UseCurrentAssembliesReferenceResolver class, in the GetReferences method... here the code that throws the exception:
return CompilerServicesUtility
.GetLoadedAssemblies()
.Where(a => !a.IsDynamic && File.Exists(a.Location) && !a.Location.Contains(CompilerServiceBase.DynamicTemplateNamespace))
.GroupBy(a => a.GetName().Name).Select(grp => grp.First(y => y.GetName().Version == grp.Max(x => x.GetName().Version))) // only select distinct assemblies based on FullName to avoid loading duplicate assemblies
.Select(a => CompilerReference.From(a))
.Concat(includeAssemblies ?? Enumerable.Empty<CompilerReference>());
Exactly the statements that throw the exception are File.Exists(a.Location) && !a.Location.Contains(CompilerServiceBase.DynamicTemplateNamespace)). The problem is that in Azure function some assemblies are protected, so no information can be retrieved about them... (surely I must study about azure functions)...
At the moment I solved writing a custom ReferenceResolver. I copied exactly the same code from the UseCurrentAssembliesReferenceResolver and I changed just the Where conditions..
So
.Where(a => !a.IsDynamic && File.Exists(a.Location) && !a.Location.Contains(CompilerServiceBase.DynamicTemplateNamespace))
became
.Where(a => !a.IsDynamic && !a.FullName.Contains("Version=0.0.0.0") && File.Exists(a.Location) && !a.Location.Contains("CompiledRazorTemplates.Dynamic"))
I am almost sure that it is not the best way to solve the problem... but now I solved it, and after two days my work is blocked I need to go on... I hope this can help someone...

Related

Application Insights - Tracking user and session across schemas

Following https://learn.microsoft.com/en-us/azure/application-insights/app-insights-usage-send-user-context, I thought it would be easy to get cross-schema tracking of a user. However, I'm finding the absolute opposite.
I created the telemetry initializer (which the document has bugs in it hardcore):
public void Initialize(ITelemetry telemetry)
{
if (HttpContext.Current?.Session == null)
return;
if (HttpContext.Current.Session["UserId"] == null)
{
HttpContext.Current.Session["UserId"] = Guid.NewGuid().ToString();
}
telemetry.Context.User.Id = (string)HttpContext.Current.Session["UserId"];
telemetry.Context.Session.Id = HttpContext.Current.Session.SessionID;
var authUser = _sessionManager.GetAuthenticatedUser<UserDetails>();
if (authUser != null)
{
telemetry.Context.User.AuthenticatedUserId = authUser.UserId;
}
}
Then I went and added it to App Insights
TelemetryConfiguration.Active.TelemetryInitializers.Add(new UserTrackingTelemetryInitializer());
I then played with my site, expecting this stuff to start showing up. It did not. I continued to get random strings for user_Id and session_Id (things like NVhLF and what not). So, I thought, okay, maybe it's logging before I update those values? I went and inserted my initializer first:
TelemetryConfiguration.Active.TelemetryInitializers.Insert(0, new UserTrackingTelemetryInitializer());
Same thing. So I started to look at schemas I don't usually look at. Nothing. So I pulled up traces and I found it. Finally, there is where my data is going. But the other schemas don't have the updated values, so what use is this? While traces is showing the expected values for user_Id and session_Id, the others continue to show garbage. Am I doing something wrong?
The document you followed does not work indeed, a feedback has been submitted here.
Just for your reference, the way I can find to set these values is that use such TrackEvent() / TrackRequest() or other Trackxxx() methods after implemented your own telemetry initializer

SharePoint CSOM - Load HasUniqueRoleAssignment fails all time

I am getting much confused as where I am doing wrong. I have done it many times before but not sure why its NOT working this time. Here is a code;
dynamic fileOrFolder;
if (model.IsFolder)
fileOrFolder = _clientContext.Web.GetFolderByServerRelativeUrl(serverRelativeUrl);
else
fileOrFolder = _clientContext.Web.GetFileByServerRelativeUrl(serverRelativeUrl);
I have tried ALL below but nothing worked;
_clientContext.Load(fileOrFolder, item => item.Include(file => file.ListItemAllFields));
dynamic blhasUniquePermission = fileOrFolder.ListItemAllFields.HasUniqueRoleAssignments;
OR
_clientContext.Load(fileOrFolder.ListItemAllFields.HasUniqueRoleAssignments);
OR
_clientContext.Load(fileOrFolder.ListItemAllFields,
items => items.Include(
item => item.Id,
item => item.DisplayName,
item => item.HasUniqueRoleAssignments));
OR
_clientContext.Load(fileOrFolder.ListItemAllFields, "Include(HasUniqueRoleAssignments)");
_clientContext.ExecuteQuery();
Everytime it is immediatly throwing error on either the Load line itself or on ExecuteQuery. We deffo know the property is there in ListItemAllFields collection then why its doing it?
It looks like you used dynamic keyword in order to hack C# into letting you create fileOrFolder variable and store instance of one of two unrelated types inside.
Not only it's weird, but by doing so you've also crippled IntelliSense and compiler.
Take this line:
_clientContext.Load(fileOrFolder, item => item.Include(file => file.ListItemAllFields));
It probably throws in runtime, because there is no Include method on neither Microsoft.SharePoint.Client.File or Microsoft.SharePoint.Client.Folder type. If you didn't use dynamic, you'd get clear compiler error instead.
This one:
_clientContext.Load(fileOrFolder.ListItemAllFields.HasUniqueRoleAssignments);
doesn't work, because _clientContext.Load takes instance of Microsoft.SharePoint.Client.ClientObject. But again, compiler doesn't know what the argument is, because it comes from dynamic object. Instead of red squiggly in editor, you get runtime error.
That should work:
ListItem itemAndOnlyItem = null;
if (model.IsFolder)
{
var folder = _clientContext.Web.GetFolderByServerRelativeUrl(serverRelativeUrl);
itemAndOnlyItem = folder.ListItemAllFields;
}
else
{
var file = _clientContext.Web.GetFileByServerRelativeUrl(serverRelativeUrl);
itemAndOnlyItem = file.ListItemAllFields;
}
_clientContext.Load(itemAndOnlyItem,
item => item.HasUniqueRoleAssignments);
_clientContext.ExecuteQuery();
var result = itemAndOnlyItem.HasUniqueRoleAssignments;

Processing an emaillist async in MVC4

I'm trying to make my MVC4-website check to see if people should be alerted with an email because they haven't done something.
I'm having a hard time figuring out how to approach this. I checked if the shared hosting platform would allow me to activate some sort of cronjob, but this is not available.
So now my idea is to perform this check on each page-request, which already seems suboptimal (because of the overhead). But I thought that with using an async it would not be in the way of people just visiting the site.
I first tried to do this in the Application_BeginRequest method in Global.asax, but then it gets called multiple times per page-request, so that didn't work.
Next I found that I can make a Global Filter which executes on OnResultExecuted, which would seemed promising, but still it's no go.
The problem I get there is that I'm using MVCMailer to send the mails, and when I execute it I get the error: {"Value cannot be null.\r\nParameter name: httpContext"}
This probably means that mailer needs the context.
The code I now have in my global filter is the following:
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
base.OnResultExecuted(filterContext);
HandleEmptyProfileAlerts();
}
private void HandleEmptyProfileAlerts()
{
new Thread(() =>
{
bool active = false;
new UserMailer().AlertFirst("bla#bla.com").Send();
DB db = new DB();
DateTime CutoffDate = DateTime.Now.AddDays(-5);
var ProfilesToAlert = db.UserProfiles.Where(x => x.CreatedOn < CutoffDate && !x.ProfileActive && x.AlertsSent.Where(y => y.AlertType == "First").Count() == 0).ToList();
foreach (UserProfile up in ProfilesToAlert)
{
if (active)
{
new UserMailer().AlertFirst(up.UserName).Send();
up.AlertsSent.Add(new UserAlert { AlertType = "First", DateSent = DateTime.Now, UserProfileID = up.UserId });
}
else
System.Diagnostics.Debug.WriteLine(up.UserName);
}
db.SaveChanges();
}).Start();
}
So my question is, am I going about this the right way, and if so, how can I make sure that MVCMailer gets the right context?
The usual way to do this kind of thing is to have a single background thread that periodically does the checks you're interested in.
You would start the thread from Application_Start(). It's common to use a database to queue and store work items, although it can also be done in memory if it's better for your app.

How do I force an object to release references when it's created by a new AppDomain and ConstructorInfo.Invoke?

Here's another one for releasing objects created by reflection:
We're working with a reporting tool (Active Reports 6) which creates a dll for each report.
We have lots of clients that use similar but still unique reports.
Reports are read through a web interface.
We run multiple sites, one for each client.
Our choices are:
1) Put all the reports in one big project which will be called by all the sites.
Cost: It will need to be recompiled every time we make a small change to any one report, potentially creating problems for all sites.
2) Create a whole bunch of similar little projects, with one for each site - let's say for sake of space that this creates problems, too.
3) Create a "Report Factory" which will use reflection to wire-up report dlls as needed.
We chose "3".
Problem: The final product works fine except for one thing: It won't release the report dll when done.
There is not currently a problem with the operation within a test environment, but if you try to do anything in the folder with the report dlls, you get the following error message: "This action can't be completed because the folder or a file in it is open in another program"
After research on this site and others, we realized that we needed an AppDomain for each call which can be cleanly unloaded.
After still having problems, we realized that the AppDomainSetup object needed to have a setting that allowed it to optimize for multiple users (LoaderOptimization.MultiDomain)
That didn't work.
Unfortunately, the base object (Active 6 report) can not be serialized, so we can't make a deep copy and chuck the original object.
After doing all of this, we're still experiencing problems.
Here is the code (C#):
private object WireUpReport(ReportArgs args)
{
//The parameter 'args' is a custom type (ReportArgs) which merely contains a
name/value pair collection.
object myReport = null;
string sPath = String.Empty;
string sFriendlyName = String.Empty;
sFriendlyName = System.Guid.NewGuid().ToString();
Assembly asmReport = null;
AppDomainSetup ads = null;
AppDomain adWireUp = null;
ConstructorInfo ci = null;
Type myReportType = null;
Type[] parametypes = null;
object[] paramarray = null;
object retObject = null;
try
{
//Get Report Object
sPath = GetWireUpPath(args); //Gets the path to the required dll; kept in a config file
//This parameter is used in an overloaded constructor further down
ads = new AppDomainSetup();
ads.ApplicationBase = Path.GetDirectoryName(sPath);
ads.LoaderOptimization = LoaderOptimization.MultiDomain;
adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
asmReport = adWireUp.GetAssemblies()[0];
asmReport = Assembly.LoadFrom(sPath);
//Create parameters for wireup
myReportType = asmReport.GetExportedTypes()[0];
parametypes = new Type[1];
parametypes[0] = typeof(ReportArgs);
ci = myReportType.GetConstructor(parametypes);
paramarray = new object[1];
paramarray[0] = args;
//Instantiate object
myReport = ci.Invoke(paramarray);
return myReport;
}
catch (Exception ex)
{
throw ex;
}
finally
{
//Make sure Assembly object is released.
if (adWireUp != null)
{
AppDomain.Unload(adWireUp);
}
if (asmReport != null)
{
asmReport = null;
}
if (ads != null)
{
ads = null;
}
if (adWireUp != null)
{
adWireUp = null;
}
if (ci != null)
{
ci = null;
}
if (myReportType != null)
{
myReportType = null;
}
if (parametypes != null)
{
parametypes = null;
}
if (paramarray != null)
{
paramarray = null;
}
}
}
The object which is returned from this code is cast as type ActiveReports and then passed around our application.
Any help would be deeply appreciated. Thanks
Your code looks like you are seriously misunderstanding how to interact with a separate AppDomain.
Think of communicating with an AppDomain like talking to someone who's currently in another country. You know where they are, but you can't just walk over and talk to them. If you want them to do something for you, you have to open up a line of communication and tell them what you need.
The way that you open that line of communication is by defining a proxy object that can be created inside the other AppDomain and then cross the boundary back to your current AppDomain. Being able to cross the boundary requires that your object either be marked as [Serializable] or inherit from MarshalByRefObject. Because we actually want to talk to a reference in the other AppDomain and not just have a copy of it, we need the proxy to do the latter.
private class CrossDomainQuery : MarshalByRefObject
{
public void LoadDataFromAssembly(string assemblyPath)
{
var assembly = Assembly.LoadFrom(assemblyPath);
//TODO: Do something with your assembly
}
}
There is a method on the AppDomain called CreateInstanceAndUnwrap() that will create an instance of that communication object inside the other AppDomain and then hand you back a __TransparentProxy object that can be cast to the proxy type.
var crossDomainQuery = (CrossDomainQuery)adWireUp.CreateInstanceAndUnwrap(
typeof(CrossDomainQuery).Assembly.FullName,
typeof(CrossDomainQuery).FullName);
Once you have that proxy object, you can call methods on it and they will be invoked in the other AppDomain.
crossDomainQuery.LoadDataFromAssembly(assemblyPath);
So how is this different from what your current example code is doing?
Your current code does not actually execute anything useful inside the other AppDomain.
adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
asmReport = adWireUp.GetAssemblies()[0];
asmReport = Assembly.LoadFrom(sPath);
This creates a new AppDomain, but then it loads all of the assemblies from that AppDomain into your current AppDomain. Additionally, it explicitly loads your report assembly into your current AppDomain.
Creating an AppDomain and calling methods on it doesn't mean that your code is executing inside of it any more than reading about another country means that you're now talking to someone inside it.
Even if you do create a proxy object and execute code inside that other AppDomain, there are a few things to be aware of.
1) Both AppDomains must be able to see the type used for the proxy, and you may have to handle AssemblyResolve events for either AppDomain manually (at least temporarily) to help resolve that.
2) AppDomains are fairly expensive to create. Generally, they are not used in situations where you need to spin something up really quickly, take some action and disappear. You should plan on either keeping them around as long as you can or be prepared to take the performance hit on every call.
3) You've said that the report type that you're instantiating is not serializable, and being able to serialize the object is a requirement for passing that type back from the other AppDomain. Defining a serializable class that can transport relevant data across the boundary and using that to pass the report data might be an option, but you'll have to determine if that works for your particular situation.
Also, as an aside, unless you have logic that depends on variables being set to null, setting everything to null in your finally does nothing useful and complicates your code.

How do you deal with the fact, that URLs are case sensitive in xPages?

How do you deal with the fact, that URLs are case sensitive in xPages even for parameters? For example URL:
my_page.xsp?folderid=785478 ... is not the same as ...
my_page.xsp?FOLDERID=785478
How to make, for example, a proper check that params contain some key e.g.
param.containsKey("folderid") which desnt work when there is 'FOLDERID' in URL.
I'd suggest defining a couple convenience #Functions:
var #HasParam = function(parameter) {
var result:boolean = false;
for (var eachParam : param.keySet()) {
if (eachParam.toLowerCase() == parameter.toLowerCase()) {
result = true;
break;
}
}
return result;
};
var #GetParam = function(parameter) {
var result = "";
if (#HasParam(parameter)) {
for (var eachParam : param.keySet()) {
if (eachParam.toLowerCase() == parameter.toLowerCase()) {
result = param.get(eachParam);
break;
}
}
}
return result;
};
Then you can safely query the parameters without caring about case. For bonus points, you could add requestScope caching so that you can skip looping through the keySet if you're examining a parameter that you've previously looked at during the same request.
you may use this function:
context.getUrlParameter('param_name')
then test if it's null or not.
make sure to decide for one,so either upper or lowercase
other than that i'd suggest something like
KeyValuePair<string,string> kvp = null;
foreach(KeyValuePair<string,string> p in param)
{
if(UPPERCASE(p.Key) == UPPERCASE("folderid"))
{
kvp = p;
break;
}
}
syntax isn't correct and idk the uppercase method in c# right now,but you get the point
The easiest answer is ofcourse the obvious. Be sure that the parameters you are using througout your application are always the same on every url you are generating and know what to expect. A good approach to accomplish this is to create a ssjs function which generates url's for you according to the objects you submit.
In this function you could check which object you are receiving and with the use of keywords and so forth generate the correct url. This way generating twice a url with the same input parameters should always generate the exact same url.
another option would be just to double check with a bit of code like this
var key = "yourkey";
if(param.contains(#uppercase(key)) || param.contains(#lowercase(key)){
// do stuff
}
But should not be necesarry if the url you are parsing is generated by your own application
Edit after post of topic starter
Another option would be to grap the url directly from from the facescontext and to convert it to a string first. When it is a string you can parse the parameters yourself.
You can combine server side substitution/redirection to get around the issue that David mentioned. So a substitution rule will redirect incoming patern like this:
http://myhost/mypage/param (/mypage/* => which converts to - /dbpath/mypage.xsp?*) - substitution is tricky so please handle with care.
Also I believe I read somewhere that context.getUrlParameter is not case sensitive - can someone please confirm this.
Hope this helps.

Resources