I'm making a Workflow designer similar to Visual Workflow Tracking*.
I would like add a new control to debug wf, but id don't know how to access to wf context
To Run WF I Use this :
WFApplication wfApp = new WorkflowApplication(act, inputs);
My idea is when i recive trace event, get context of wfApp, to get vars or arguments values.
It's posibble?
*You can donwload VisualStudioTracking Code From :
Windows Communication Foundation (WCF) and Windows Workflow Foundation (WF) Samples for .NET Framework 4
and then
:\WF_WCF_Samples\WF\Application\VisualWorkflowTracking*
Finally I solved the problem.
First I get all arguments names, and variables of workflow, from xaml.
class XamlHelper
{
private string xaml;
public XamlHelper(string xaml)
{
this.xaml = xaml;
DynamicActivity act = GetRuntimeExecutionRoot(this.xaml);
ArgumentNames = GetArgumentsNames(act);
GetVariables(act);
}
private void GetVariables(DynamicActivity act)
{
Variables = new List<string>();
InspectActivity(act);
}
private void InspectActivity(Activity root)
{
IEnumerator<Activity> activities =
WorkflowInspectionServices.GetActivities(root).GetEnumerator();
while (activities.MoveNext())
{
PropertyInfo propVars = activities.Current.GetType().GetProperties().FirstOrDefault(p => p.Name == "Variables" && p.PropertyType == typeof(Collection<Variable>));
if (propVars != null)
{
try
{
Collection<Variable> variables = (Collection<Variable>)propVars.GetValue(activities.Current, null);
variables.ToList().ForEach(v =>
{
Variables.Add(v.Name);
});
}
catch
{
}
}
InspectActivity(activities.Current);
}
}
public List<string> Variables
{
get;
private set;
}
public List<string> ArgumentNames
{
get;
private set;
}
private DynamicActivity GetRuntimeExecutionRoot(string xaml)
{
Activity root = ActivityXamlServices.Load(new StringReader(xaml));
WorkflowInspectionServices.CacheMetadata(root);
return root as DynamicActivity;
}
private List<string> GetArgumentsNames(DynamicActivity act)
{
List<string> names = new List<string>();
if (act != null)
{
act.Properties.Where(p => typeof(Argument).IsAssignableFrom(p.Type)).ToList().ForEach(dp =>
{
names.Add(dp.Name);
});
}
return names;
}
}
Second I create trace with these arguments and variable names
private static WFTrace CreateTrace(List<string> argumentNames, List<string> variablesNames)
{
WFTrace trace = new WFTrace();
trace.TrackingProfile = new TrackingProfile()
{
ImplementationVisibility = ImplementationVisibility.All,
Name = "CustomTrackingProfile",
Queries =
{
new CustomTrackingQuery()
{
Name = all,
ActivityName = all
},
new WorkflowInstanceQuery()
{
// Limit workflow instance tracking records for started and completed workflow states
States = {WorkflowInstanceStates.Started, WorkflowInstanceStates.Completed },
}
}
};
trace.TrackingProfile.Queries.Add(GetActivityQueryState(argumentNames, variablesNames));
return trace;
}
And then invoke wf and add traceextension.
Adam this is the code
private TrackingQuery GetActivityQueryState(List<string> argumentNames, List<string> variablesNames)
{
ActivityStateQuery query = new ActivityStateQuery()
{
ActivityName = "*",
States = { ActivityStates.Executing, ActivityStates.Closed }
};
if (argumentNames != null)
{
argumentNames.Distinct().ToList().ForEach(arg =>
{
query.Arguments.Add(arg);
});
}
if (variablesNames != null)
{
variablesNames.Distinct().ToList().ForEach(v =>
{
query.Variables.Add(v);
});
}
return query;
}
You can use a Tracking Participants to extract the variables and arguments when the workflow is running.
I try tracking variables with this tracking participant
private static VisualTrackingParticipant VisualTracking()
{
String all = "*";
VisualTrackingParticipant simTracker = new VisualTrackingParticipant()
{
TrackingProfile = new TrackingProfile()
{
Name = "CustomTrackingProfile",
Queries =
{
new CustomTrackingQuery()
{
Name = all,
ActivityName = all
},
new WorkflowInstanceQuery()
{
// Limit workflow instance tracking records for started and completed workflow states
// States = { WorkflowInstanceStates.Started, WorkflowInstanceStates.Completed },
States={all}
},
new ActivityStateQuery()
{
// Subscribe for track records from all activities for all states
ActivityName = all,
States = { all },
// Extract workflow variables and arguments as a part of the activity tracking record
// VariableName = "*" allows for extraction of all variables in the scope
// of the activity
Variables =
{
{ all }
},
Arguments={ {all}}
}
}
}
};
return simTracker;
}
and this is VisualTrackingParticipant
public class VisualTrackingParticipant : System.Activities.Tracking.TrackingParticipant
{
public event EventHandler<TrackingEventArgs> TrackingRecordReceived;
public Dictionary<string, Activity> ActivityIdToWorkflowElementMap { get; set; }
protected override void Track(TrackingRecord record, TimeSpan timeout)
{
OnTrackingRecordReceived(record, timeout);
}
//On Tracing Record Received call the TrackingRecordReceived with the record received information from the TrackingParticipant.
//We also do not worry about Expressions' tracking data
protected void OnTrackingRecordReceived(TrackingRecord record, TimeSpan timeout)
{
System.Diagnostics.Debug.WriteLine(
String.Format("Tracking Record Received: {0} with timeout: {1} seconds.", record, timeout.TotalSeconds)
);
if (TrackingRecordReceived != null)
{
ActivityStateRecord activityStateRecord = record as ActivityStateRecord;
if//(
(activityStateRecord != null)
// && (!activityStateRecord.Activity.TypeName.Contains("System.Activities.Expressions")))
{
Activity act = null;
ActivityIdToWorkflowElementMap.TryGetValue(activityStateRecord.Activity.Id, out act);
TrackingRecordReceived(this, new TrackingEventArgs(
record,
timeout,
act
)
);
}
else
{
TrackingRecordReceived(this, new TrackingEventArgs(record, timeout, null));
}
}
}
}
//Custom Tracking EventArgs
public class TrackingEventArgs : EventArgs
{
public TrackingRecord Record {get; set;}
public TimeSpan Timeout {get; set;}
public Activity Activity { get; set; }
public TrackingEventArgs(TrackingRecord trackingRecord, TimeSpan timeout, Activity activity)
{
this.Record = trackingRecord;
this.Timeout = timeout;
this.Activity = activity;
}
}
if I trace this wf :
<Activity mc:Ignorable="sap" x:Class="Microsoft.Samples.Workflow" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:av="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:s1="clr-namespace:System;assembly=System" xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=System.Core" xmlns:sa="clr-namespace:System.Activities;assembly=System.Activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sd1="clr-namespace:System.Data;assembly=System.Data.DataSetExtensions" xmlns:sl="clr-namespace:System.Linq;assembly=System.Core" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:Members>
<x:Property Name="decisionVar" Type="InArgument(x:Boolean)" />
</x:Members>
<sap:VirtualizedContainerService.HintSize>666,676</sap:VirtualizedContainerService.HintSize>
<mva:VisualBasic.Settings>Assembly references and imported namespaces serialized as XML namespaces</mva:VisualBasic.Settings>
<Flowchart sad:XamlDebuggerXmlReader.FileName="C:\WF_WCF_Samples\WF\Application\VisualWorkflowTracking\CS\VisualWorkflowTracking\Workflow.xaml" sap:VirtualizedContainerService.HintSize="626,636" mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces">
<Flowchart.Variables>
<Variable x:TypeArguments="x:String" Name="myStr" />
</Flowchart.Variables>
<sap:WorkflowViewStateService.ViewState>
<scg3:Dictionary x:TypeArguments="x:String, x:Object">
<x:Boolean x:Key="IsExpanded">False</x:Boolean>
<av:Point x:Key="ShapeLocation">270,7.5</av:Point>
<av:Size x:Key="ShapeSize">60,75</av:Size>
<av:PointCollection x:Key="ConnectorLocation">300,82.5 300,111.5</av:PointCollection>
<x:Double x:Key="Width">611.5</x:Double>
</scg3:Dictionary>
</sap:WorkflowViewStateService.ViewState>
<Flowchart.StartNode>
<FlowStep x:Name="__ReferenceID0">
<sap:WorkflowViewStateService.ViewState>
<scg3:Dictionary x:TypeArguments="x:String, x:Object">
<av:Point x:Key="ShapeLocation">179,111.5</av:Point>
<av:Size x:Key="ShapeSize">242,58</av:Size>
<av:PointCollection x:Key="ConnectorLocation">300,169.5 300,199.5 280,199.5 280,269.5</av:PointCollection>
</scg3:Dictionary>
</sap:WorkflowViewStateService.ViewState>
<Assign sap:VirtualizedContainerService.HintSize="242,58">
<Assign.To>
<OutArgument x:TypeArguments="x:String">[myStr]</OutArgument>
</Assign.To>
<Assign.Value>
<InArgument x:TypeArguments="x:String">PDC Rocks</InArgument>
</Assign.Value>
</Assign>
<FlowStep.Next>
<FlowStep x:Name="__ReferenceID1">
<sap:WorkflowViewStateService.ViewState>
<scg3:Dictionary x:TypeArguments="x:String, x:Object">
<av:Point x:Key="ShapeLocation">174.5,269.5</av:Point>
<av:Size x:Key="ShapeSize">211,61</av:Size>
<av:PointCollection x:Key="ConnectorLocation">155.33,351.361666666667 155.33,481 204.5,481</av:PointCollection>
</scg3:Dictionary>
</sap:WorkflowViewStateService.ViewState>
<WriteLine sap:VirtualizedContainerService.HintSize="211,61" Text="[myStr]" />
</FlowStep>
</FlowStep.Next>
</FlowStep>
</Flowchart.StartNode>
<x:Reference>__ReferenceID0</x:Reference>
<x:Reference>__ReferenceID1</x:Reference>
</Flowchart>
</Activity>
When activitity assing close y recive this trace this args
[Arg] Value = PDC Rocks
[Arg] To = PDC Rocks
and I don't know the name of var where value is assined
Related
I am using Asp.Net core 3.1 . If I want to create a WebSockets backend for example for
a chat app , I need to store all the related WebSocket objects for broadcasting events , my question is what is the best way to manage removing objects that are no longer useful (if disconnected or no longer open). keeping in mind that I want other parts of the application to access the WebScoket groups to also broadcast events if needed. I store the related connections in a ConnectionNode which is the nearest layer to the Websocket objects , a class called WebsocketsManager manage these nodes, a service in the background runs to clear the unused objects every timeout period. but since I want the group(related connections)to be accessible for the application (for example other endpoints); to avoid any concurrent modification errors , if a broadcast is required during the cleaning process,the broadcast will have to wait for the cleaning process to finish, thats why the WebsocketsManager if the related connections are larger than a certain limit it will divide them into multiple related ConnectionNodes , that way the cleaning process can continue partially for related connection while broadcasting if needed. I want to know how good my solution will behave or what is the best way to do it. any help would be really appreciated.
ConnectionNode
public class ConnectionNode
{
private List<WebSocket> connections;
private BroadcastQueue BroadcastQueue = new BroadcastQueue();
private bool isBroadCasting = false;
private bool isCleaning = false;
public void AddConnection(WebSocket socket)
{
if (connections == null)
connections = new List<WebSocket>();
connections.Add(socket);
}
public void Broadcast(Broadcast broadCast)
{
while (isCleaning)
{
}
BroadcastQueue.QueueBroadcast(broadCast);
if (isBroadCasting)
{
return;
}
isBroadCasting = true;
var broadcast = BroadcastQueue.GetNext();
while (broadCast != null)
{
foreach (var ws in connections)
{
broadCast.Dispatch(ws);
}
broadCast = BroadcastQueue.GetNext();
}
isBroadCasting = false;
}
public int CleanUnUsedConnections()
{
if (isBroadCasting)
return 0;
isCleaning = true;
var i =connections.RemoveAll(s => s.State != WebSocketState.Open);
isCleaning = false;
return i;
}
public int ConnectionsCount()
{
return connections.Count;
}
}
Manager class
public class WebSocketsManager
{
static int ConnectionNodesDividerLimit = 1000;
private ConcurrentDictionary<String, List<ConnectionNode>> mConnectionNodes;
private readonly ILogger<WebSocketsManager> logger;
public WebSocketsManager(ILogger<WebSocketsManager> logger)
{
this.logger = logger;
}
public ConnectionNode RequireNode(string Id)
{
if (mConnectionNodes == null)
mConnectionNodes = new ConcurrentDictionary<String, List<ConnectionNode>>();
var node = mConnectionNodes.GetValueOrDefault(Id);
if (node == null)
{
node = new List<ConnectionNode>();
node.Add(new ConnectionNode());
mConnectionNodes.TryAdd(Id, node);
return node[0];
}
if (ConnectionNodesDividerLimit != 0)
{
if (node[0].ConnectionsCount() == ConnectionNodesDividerLimit)
{
node.Insert(0,new ConnectionNode());
}
}
return node[0];
}
public void ClearUnusedConnections()
{
logger.LogInformation("Manager is Clearing ..");
if (mConnectionNodes == null)
return;
if (mConnectionNodes.IsEmpty)
{
logger.LogInformation("Empty ## Nothing to clear ..");
return;
}
Dictionary<String,ConnectionNode> ToBeRemovedNodes = new Dictionary<String, ConnectionNode>();
foreach (var pair in mConnectionNodes)
{
bool shoudlRemoveStack = true;
foreach (var node in pair.Value)
{
int i = node.CleanUnUsedConnections();
logger.LogInformation($"Removed ${i} from connection node(s){pair.Key}");
if (node.ConnectionsCount() == 0)
{
ToBeRemovedNodes[pair.Key] = node;
logger.LogInformation($"To be Removed A node From ..{pair.Key}");
}
else
{
shoudlRemoveStack = false;
}
}
if (shoudlRemoveStack)
{
ToBeRemovedNodes.Remove(pair.Key);
List<ConnectionNode> v =null;
var b = mConnectionNodes.TryRemove(pair.Key,out v);
logger.LogInformation($"Removing the Stack ..{pair.Key} Removed ${b}");
}
}
foreach (var pair in ToBeRemovedNodes)
{
mConnectionNodes[pair.Key].Remove(pair.Value);
logger.LogInformation($"Clearing Nodes : Clearing Nodes from stack #{pair.Key}");
}
}
public void Broadcast(string id, Broadcast broadcast)
{
var c = mConnectionNodes.GetValueOrDefault(id);
foreach (var node in c)
{
node.Broadcast(broadcast);
}
}
the service
public class SocketsConnectionsCleaningService : BackgroundService
{
private readonly IServiceProvider Povider;
private Timer Timer = null;
private bool isRunning = false;
private readonly ILogger Logger;
public SocketsConnectionsCleaningService(IServiceProvider Provider, ILogger<SocketsConnectionsCleaningService> Logger )
{
this.Povider = Provider;
this.Logger = Logger;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
Logger.LogInformation("Execute Sync is called ");
Timer = new Timer(DeleteClosedConnections, null, TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(2));
return Task.CompletedTask;
}
private void DeleteClosedConnections(object state)
{
Logger.LogInformation("Clearing ");
if (isRunning)
{
Logger.LogInformation("A Task is Running Return ");
return;
}
isRunning = true;
var connectionManager = Povider.GetService(typeof(WebSocketsManager)) as WebSocketsManager;
connectionManager.ClearUnusedConnections();
isRunning = false;
Logger.LogInformation($"Finished Cleaning !");
}
}
Usage in a controller be like
[HttpGet("ws")]
public async Task SomeRealtimeFunction()
{
if (HttpContext.IsWebSocketsRequest())
{
using var socket = await HttpContext.AcceptSocketRequest();
try
{
await socket.SendString(" Connected! ");
webSocketsManager.RequireNode("Chat Room")
.AddConnection(socket);
var RecieverHelper = socket.GetRecieveResultsHelper();
string str = await RecieverHelper.ReceiveString();
while (!RecieverHelper.Result.CloseStatus.HasValue)
{
webSocketsManager
.Broadcast("Chat Room", new StringBroadcast(str));
str = await RecieverHelper.ReceiveString();
}
}
catch (Exception e)
{
await socket.SendString("Error!");
await socket.SendString(e.Message);
await socket.SendString(e.ToString());
}
}
else
{
HttpContext.Response.StatusCode = 400;
}
}
I am trying to write a REST web service using ServiceStack that accepts variable paths off of route. For example:
[Route("/{group}"]
public class Entity : IReturn<SomeType> {}
This throws a NotSupported Exception "RestPath '/{collection}' on type Entity is not supported". However, if I change the path as follows (along with the associated path in AppHost configuration) to:
[Route("/rest/{group}"]
It works just fine. In order to integrate with the system that I am working with, I need to use /{group}.
ServiceStack now allows you to add a fallback route from the / root path to handle any un-matched requests, that's not handled by a catch-all handler or refers to an existing static file. So in v3.9.56 you can now do:
[FallbackRoute("/{group}"]
public class Entity : IReturn<SomeType> {}
An alternative option is to register a IAppHost.CatchAllHandlers to handle un-matched routes, but you would need to return your own IHttpHandler to handle the request or alternatively return a RedirectHttpHandler to redirect to a different route that is managed by ServiceStack.
My current work in progress, a plugin to serve the default page to all 'not found' routes without changing the url in the browser, includes most of what you'll need to handle a global wildcard route. Use it to get you started.
To understand what this code is doing, it helps to understand ServiceStack's routing priority, and how CatchAllHandlers fit into the process. ServiceStack calls ServiceStackHttpHandlerFactory.GetHandler to get the handler for the current route.
ServiceStackHttpHandlerFactory.GetHandler returns:
A matching RawHttpHandler, if any.
If the domain root, the handler returned by GetCatchAllHandlerIfAny(...), if any.
If the route matches a metadata uri (I'm skipping over the exact logic here, as it's not important for your question), the relevant handler, if any.
The handler returned by ServiceStackHttpHandlerFactory.GetHandlerForPathInfo if any.
NotFoundHandler.
ServiceStackHttpHandlerFactory.GetHandlerForPathInfo returns:
If the url matches a valid REST route, a new RestHandler.
If the url matches an existing file or directory, it returns
the handler returned by GetCatchAllHandlerIfAny(...), if any.
If it's a supported filetype, a StaticFileHandler,
If it's not a supported filetype, the ForbiddenHttpHandler.
The handler returned by GetCatchAllHandlerIfAny(...), if any.
null.
The CatchAllHandlers array contains functions that evaluate the url and either return a handler, or null. The functions in the array are called in sequence and the first one that doesn't return null handles the route. Let me highlight some key elements:
First, the plugin adds a CatchAllHandler to the appHost.CatchAllHandlers array when registered.
public void Register(IAppHost appHost)
{
appHost.CatchAllHandlers.Add((string method, string pathInfo, string filepath) =>
Factory(method, pathInfo, filepath));
}
Second, the CatchAllHandler. As described above, the function may be called for the domain root, an existing file or directory, or any other unmatched route. Your method should return a handler, if your criteria are met, or return null.
private static Html5ModeFeature Factory(String method, String pathInfo, String filepath)
{
var Html5ModeHandler = Html5ModeFeature.Instance;
List<string> WebHostRootFileNames = RootFiles();
// handle domain root
if (string.IsNullOrEmpty(pathInfo) || pathInfo == "/")
{
return Html5ModeHandler;
}
// don't handle 'mode' urls
var mode = EndpointHost.Config.ServiceStackHandlerFactoryPath;
if (mode != null && pathInfo.EndsWith(mode))
{
return null;
}
var pathParts = pathInfo.TrimStart('/').Split('/');
var existingFile = pathParts[0].ToLower();
var catchAllHandler = new Object();
if (WebHostRootFileNames.Contains(existingFile))
{
var fileExt = Path.GetExtension(filepath);
var isFileRequest = !string.IsNullOrEmpty(fileExt);
// don't handle directories or files that have another handler
catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath);
if (catchAllHandler != null) return null;
// don't handle existing files under any event
return isFileRequest ? null : Html5ModeHandler;
}
// don't handle non-physical urls that have another handler
catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath);
if (catchAllHandler != null) return null;
// handle anything else
return Html5ModeHandler;
}
In the case of the wildcard at the root domain, you may not want to hijack routes that can be handled by another CatchAllHandler. If so, to avoid infinite recursion, you'll need a custom GetCatchAllHandlerIfAny method.
//
// local copy of ServiceStackHttpHandlerFactory.GetCatchAllHandlerIfAny, prevents infinite recursion
//
private static IHttpHandler GetCatchAllHandlerIfAny(string httpMethod, string pathInfo, string filePath)
{
if (EndpointHost.CatchAllHandlers != null)
{
foreach (var httpHandlerResolver in EndpointHost.CatchAllHandlers)
{
if (httpHandlerResolver == Html5ModeFeature.Factory) continue; // avoid infinite recursion
var httpHandler = httpHandlerResolver(httpMethod, pathInfo, filePath);
if (httpHandler != null)
return httpHandler;
}
}
return null;
}
Here's the complete, and completely untested, plugin. It compiles. It carries no warranty of fitness for any specific purpose.
using ServiceStack;
using ServiceStack.Common.Web;
using ServiceStack.Razor;
using ServiceStack.ServiceHost;
using ServiceStack.Text;
using ServiceStack.WebHost.Endpoints;
using ServiceStack.WebHost.Endpoints.Formats;
using ServiceStack.WebHost.Endpoints.Support;
using ServiceStack.WebHost.Endpoints.Support.Markdown;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;
namespace MyProject.Support
{
public enum DefaultFileFormat
{
Markdown,
Razor,
Static
}
public class Html5ModeFeature : EndpointHandlerBase, IPlugin
{
private FileInfo fi { get; set; }
private DefaultFileFormat FileFormat { get; set; }
private DateTime FileModified { get; set; }
private byte[] FileContents { get; set; }
public MarkdownHandler Markdown { get; set; }
public RazorHandler Razor { get; set; }
public object Model { get; set; }
private static Dictionary<string, string> allDirs;
public string PathInfo { get; set; }
public void Register(IAppHost appHost)
{
appHost.CatchAllHandlers.Add((string method, string pathInfo, string filepath) =>
Factory(method, pathInfo, filepath));
}
private Html5ModeFeature()
{
foreach (var defaultDoc in EndpointHost.Config.DefaultDocuments)
{
if (PathInfo == null)
{
var defaultFileName = Path.Combine(Directory.GetCurrentDirectory(), defaultDoc);
if (!File.Exists(defaultFileName)) continue;
PathInfo = (String)defaultDoc; // use first default document found.
}
}
SetFile();
}
private static Html5ModeFeature instance;
public static Html5ModeFeature Instance
{
get { return instance ?? (instance = new Html5ModeFeature()); }
}
public void SetFile()
{
if (PathInfo.EndsWith(MarkdownFormat.MarkdownExt) || PathInfo.EndsWith(MarkdownFormat.TemplateExt))
{
Markdown = new MarkdownHandler(PathInfo);
FileFormat = DefaultFileFormat.Markdown;
return;
}
if (PathInfo.EndsWith(Razor.RazorFormat.RazorFileExtension)) {
Razor = new RazorHandler(PathInfo);
FileFormat = DefaultFileFormat.Razor;
return;
}
FileContents = File.ReadAllBytes(PathInfo);
FileModified = File.GetLastWriteTime(PathInfo);
FileFormat = DefaultFileFormat.Static;
}
//
// ignore request.PathInfo, return default page, extracted from StaticFileHandler.ProcessResponse
//
public void ProcessStaticPage(IHttpRequest request, IHttpResponse response, string operationName)
{
response.EndHttpHandlerRequest(skipClose: true, afterBody: r =>
{
TimeSpan maxAge;
if (r.ContentType != null && EndpointHost.Config.AddMaxAgeForStaticMimeTypes.TryGetValue(r.ContentType, out maxAge))
{
r.AddHeader(HttpHeaders.CacheControl, "max-age=" + maxAge.TotalSeconds);
}
if (request.HasNotModifiedSince(fi.LastWriteTime))
{
r.ContentType = MimeTypes.GetMimeType(PathInfo);
r.StatusCode = 304;
return;
}
try
{
r.AddHeaderLastModified(fi.LastWriteTime);
r.ContentType = MimeTypes.GetMimeType(PathInfo);
if (fi.LastWriteTime > this.FileModified)
SetFile(); //reload
r.OutputStream.Write(this.FileContents, 0, this.FileContents.Length);
r.Close();
return;
}
catch (Exception ex)
{
throw new HttpException(403, "Forbidden.");
}
});
}
private void ProcessServerError(IHttpRequest httpReq, IHttpResponse httpRes, string operationName)
{
var sb = new StringBuilder();
sb.AppendLine("{");
sb.AppendLine("\"ResponseStatus\":{");
sb.AppendFormat(" \"ErrorCode\":{0},\n", 500);
sb.AppendFormat(" \"Message\": HTML5ModeHandler could not serve file {0}.\n", PathInfo.EncodeJson());
sb.AppendLine("}");
sb.AppendLine("}");
httpRes.EndHttpHandlerRequest(skipClose: true, afterBody: r =>
{
r.StatusCode = 500;
r.ContentType = ContentType.Json;
var sbBytes = sb.ToString().ToUtf8Bytes();
r.OutputStream.Write(sbBytes, 0, sbBytes.Length);
r.Close();
});
return;
}
private static List<string> RootFiles()
{
var WebHostPhysicalPath = EndpointHost.Config.WebHostPhysicalPath;
List<string> WebHostRootFileNames = new List<string>();
foreach (var filePath in Directory.GetFiles(WebHostPhysicalPath))
{
var fileNameLower = Path.GetFileName(filePath).ToLower();
WebHostRootFileNames.Add(Path.GetFileName(fileNameLower));
}
foreach (var dirName in Directory.GetDirectories(WebHostPhysicalPath))
{
var dirNameLower = Path.GetFileName(dirName).ToLower();
WebHostRootFileNames.Add(Path.GetFileName(dirNameLower));
}
return WebHostRootFileNames;
}
private static Html5ModeFeature Factory(String method, String pathInfo, String filepath)
{
var Html5ModeHandler = Html5ModeFeature.Instance;
List<string> WebHostRootFileNames = RootFiles();
// handle domain root
if (string.IsNullOrEmpty(pathInfo) || pathInfo == "/")
{
return Html5ModeHandler;
}
// don't handle 'mode' urls
var mode = EndpointHost.Config.ServiceStackHandlerFactoryPath;
if (mode != null && pathInfo.EndsWith(mode))
{
return null;
}
var pathParts = pathInfo.TrimStart('/').Split('/');
var existingFile = pathParts[0].ToLower();
var catchAllHandler = new Object();
if (WebHostRootFileNames.Contains(existingFile))
{
var fileExt = Path.GetExtension(filepath);
var isFileRequest = !string.IsNullOrEmpty(fileExt);
// don't handle directories or files that have another handler
catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath);
if (catchAllHandler != null) return null;
// don't handle existing files under any event
return isFileRequest ? null : Html5ModeHandler;
}
// don't handle non-physical urls that have another handler
catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath);
if (catchAllHandler != null) return null;
// handle anything else
return Html5ModeHandler;
}
//
// Local copy of private StaticFileHandler.DirectoryExists
//
public static bool DirectoryExists(string dirPath, string appFilePath)
{
if (dirPath == null) return false;
try
{
if (!ServiceStack.Text.Env.IsMono)
return Directory.Exists(dirPath);
}
catch
{
return false;
}
if (allDirs == null)
allDirs = CreateDirIndex(appFilePath);
var foundDir = allDirs.ContainsKey(dirPath.ToLower());
//log.DebugFormat("Found dirPath {0} in Mono: ", dirPath, foundDir);
return foundDir;
}
//
// Local copy of private StaticFileHandler.CreateDirIndex
//
static Dictionary<string, string> CreateDirIndex(string appFilePath)
{
var indexDirs = new Dictionary<string, string>();
foreach (var dir in GetDirs(appFilePath))
{
indexDirs[dir.ToLower()] = dir;
}
return indexDirs;
}
//
// Local copy of private StaticFileHandler.GetDirs
//
static List<string> GetDirs(string path)
{
var queue = new Queue<string>();
queue.Enqueue(path);
var results = new List<string>();
while (queue.Count > 0)
{
path = queue.Dequeue();
try
{
foreach (string subDir in Directory.GetDirectories(path))
{
queue.Enqueue(subDir);
results.Add(subDir);
}
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
}
}
return results;
}
//
// local copy of ServiceStackHttpHandlerFactory.GetCatchAllHandlerIfAny, prevents infinite recursion
//
private static IHttpHandler GetCatchAllHandlerIfAny(string httpMethod, string pathInfo, string filePath)
{
if (EndpointHost.CatchAllHandlers != null)
{
foreach (var httpHandlerResolver in EndpointHost.CatchAllHandlers)
{
if (httpHandlerResolver == Html5ModeFeature.Factory) continue; // avoid infinite recursion
var httpHandler = httpHandlerResolver(httpMethod, pathInfo, filePath);
if (httpHandler != null)
return httpHandler;
}
}
return null;
}
public override void ProcessRequest(IHttpRequest httpReq, IHttpResponse httpRes, string operationName)
{
switch (FileFormat)
{
case DefaultFileFormat.Markdown:
{
Markdown.ProcessRequest(httpReq, httpRes, operationName);
break;
}
case DefaultFileFormat.Razor:
{
Razor.ProcessRequest(httpReq, httpRes, operationName);
break;
}
case DefaultFileFormat.Static:
{
fi.Refresh();
if (fi.Exists) ProcessStaticPage(httpReq, httpRes, operationName); else ProcessServerError(httpReq, httpRes, operationName);
break;
}
default:
{
ProcessServerError(httpReq, httpRes, operationName);
break;
}
}
}
public override object CreateRequest(IHttpRequest request, string operationName)
{
return null;
}
public override object GetResponse(IHttpRequest httpReq, IHttpResponse httpRes, object request)
{
return null;
}
}
}
NEventStore 3.2.0.0
As far as I found out it is required by NEventStore that old event-types must kept around for event up-conversion.
To keep them deserializing correctly in the future they must have an unique name. It is suggested to call it like EventEVENT_VERSION.
Is there any way to avoid EventV1, EventV2,..., EventVN cluttering up your domain model and simply keep using Event?
What are your strategies?
In a question long, long time ago, an answer was missing...
In the discussion referred in the comments, I came up with an - I would say - elegant solution:
Don't save the type-name but an (versioned) identifier
The identifier is set by an attribute on class-level, i.e.
namespace CurrentEvents
{
[Versioned("EventSomethingHappened", 0)] // still version 0
public class EventSomethingHappened
{
...
}
}
This identifier should get serialized in/beside the payload. In serialized form
"Some.Name.Space.EventSomethingHappened" -> "EventSomethingHappened|0"
When another version of this event is required, the current version is copied in an "legacy" assembly or just in another Namespace and renamed (type-name) to "EventSomethingHappenedV0" - but the Versioned-attribute remains untouched (in this copy)
namespace LegacyEvents
{
[Versioned("EventSomethingHappened", 0)] // still version 0
public class EventSomethingHappenedV0
{
...
}
}
In the new version (at the same place, under the same name) just the version-part of the attribute gets incremented. And that's it!
namespace CurrentEvents
{
[Versioned("EventSomethingHappened", 1)] // new version 1
public class EventSomethingHappened
{
...
}
}
Json.NET supports binders which maps type-identifiers to types and back. Here is a production-ready binder:
public class VersionedSerializationBinder : DefaultSerializationBinder
{
private Dictionary<string, Type> _getImplementationLookup = new Dictionary<string, Type>();
private static Type[] _versionedEvents = null;
protected static Type[] VersionedEvents
{
get
{
if (_versionedEvents == null)
_versionedEvents = AppDomain.CurrentDomain.GetAssemblies()
.Where(x => x.IsDynamic == false)
.SelectMany(x => x.GetExportedTypes()
.Where(y => y.IsAbstract == false &&
y.IsInterface == false))
.Where(x => x.GetCustomAttributes(typeof(VersionedAttribute), false).Any())
.ToArray();
return _versionedEvents;
}
}
public VersionedSerializationBinder()
{
}
private VersionedAttribute GetVersionInformation(Type type)
{
var attr = type.GetCustomAttributes(typeof(VersionedAttribute), false).Cast<VersionedAttribute>().FirstOrDefault();
return attr;
}
public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
var versionInfo = GetVersionInformation(serializedType);
if (versionInfo != null)
{
var impl = GetImplementation(versionInfo);
typeName = versionInfo.Identifier + "|" + versionInfo.Revision;
}
else
{
base.BindToName(serializedType, out assemblyName, out typeName);
}
assemblyName = null;
}
private VersionedAttribute GetVersionInformation(string serializedInfo)
{
var strs = serializedInfo.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
if (strs.Length != 2)
return null;
return new VersionedAttribute(strs[0], strs[1]);
}
public override Type BindToType(string assemblyName, string typeName)
{
if (typeName.Contains('|'))
{
var type = GetImplementation(GetVersionInformation(typeName));
if (type == null)
throw new InvalidOperationException(string.Format("VersionedEventSerializationBinder: No implementation found for type identifier '{0}'", typeName));
return type;
}
else
{
var versionInfo = GetVersionInformation(typeName + "|0");
if (versionInfo != null)
{
var type = GetImplementation(versionInfo);
if (type != null)
return type;
// else: continue as it is a normal serialized object...
}
}
// resolve assembly name if not in serialized info
if (string.IsNullOrEmpty(assemblyName))
{
Type type;
if (typeName.TryFindType(out type))
{
assemblyName = type.Assembly.GetName().Name;
}
}
return base.BindToType(assemblyName, typeName);
}
private Type GetImplementation(VersionedAttribute attribute)
{
Type eventType = null;
if (_getImplementationLookup.TryGetValue(attribute.Identifier + "|" + attribute.Revision, out eventType) == false)
{
var events = VersionedEvents
.Where(x =>
{
return x.GetCustomAttributes(typeof(VersionedAttribute), false)
.Cast<VersionedAttribute>()
.Where(y =>
y.Revision == attribute.Revision &&
y.Identifier == attribute.Identifier)
.Any();
})
.ToArray();
if (events.Length == 0)
{
eventType = null;
}
else if (events.Length == 1)
{
eventType = events[0];
}
else
{
throw new InvalidOperationException(
string.Format("VersionedEventSerializationBinder: Multiple types have the same VersionedEvent attribute '{0}|{1}':\n{2}",
attribute.Identifier,
attribute.Revision,
string.Join(", ", events.Select(x => x.FullName))));
}
_getImplementationLookup[attribute.Identifier + "|" + attribute.Revision] = eventType;
}
return eventType;
}
}
...and the Versioned-attribute
[AttributeUsage(AttributeTargets.Class)]
public class VersionedAttribute : Attribute
{
public string Revision { get; set; }
public string Identifier { get; set; }
public VersionedAttribute(string identifier, string revision = "0")
{
this.Identifier = identifier;
this.Revision = revision;
}
public VersionedAttribute(string identifier, long revision)
{
this.Identifier = identifier;
this.Revision = revision.ToString();
}
}
At last use the versioned binder like this
JsonSerializer.Create(new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
TypeNameAssemblyFormat = FormatterAssemblyStyle.Simple,
Binder = new VersionedSerializationBinder()
});
For a full Json.NET ISerialize-implementation see (an little outdated) gist here:
https://gist.github.com/warappa/6388270
"How can i use engine in my console application"
I shouldn't use the ITemplate-interface and Transform-Method.
I am using Tridion 2011
Could anyone please suggest me.
You can't. The Engine class is part of the TOM.NET and that API is explicitly reserved for use in:
Template Building Blocks
Event Handlers
For all other cases (such as console applications) you should use the Core Service.
There are many good questions (and articles on other web sites) already:
https://stackoverflow.com/search?q=%5Btridion%5D+core+service
http://www.google.com/#q=tridion+core+service
If you get stuck along the way, show us the relevant code+configuration you have and what error message your get (or at what step you are stuck) and we'll try to help from there.
From a console application you should use the Core Service. I wrote a small example using the Core Service to search for items in the content manager.
Console.WriteLine("FullTextQuery:");
var fullTextQuery = Console.ReadLine();
if (String.IsNullOrWhiteSpace(fullTextQuery) || fullTextQuery.Equals(":q", StringComparison.OrdinalIgnoreCase))
{
break;
}
Console.WriteLine("SearchIn IdRef:");
var searchInIdRef = Console.ReadLine();
var queryData = new SearchQueryData
{
FullTextQuery = fullTextQuery,
SearchIn = new LinkToIdentifiableObjectData
{
IdRef = searchInIdRef
}
};
var results = coreServiceClient.GetSearchResults(queryData);
results.ToList().ForEach(result => Console.WriteLine("{0} ({1})", result.Title, result.Id));
Add a reference to Tridion.ContentManager.CoreService.Client to your Visual Studio Project.
Code of the Core Service Client Provider:
public interface ICoreServiceProvider
{
CoreServiceClient GetCoreServiceClient();
}
public class CoreServiceDefaultProvider : ICoreServiceProvider
{
private CoreServiceClient _client;
public CoreServiceClient GetCoreServiceClient()
{
return _client ?? (_client = new CoreServiceClient());
}
}
And the client itself:
public class CoreServiceClient : IDisposable
{
public SessionAwareCoreServiceClient ProxyClient;
private const string DefaultEndpointName = "netTcp_2011";
public CoreServiceClient(string endPointName)
{
if(string.IsNullOrWhiteSpace(endPointName))
{
throw new ArgumentNullException("endPointName", "EndPointName is not specified.");
}
ProxyClient = new SessionAwareCoreServiceClient(endPointName);
}
public CoreServiceClient() : this(DefaultEndpointName) { }
public string GetApiVersionNumber()
{
return ProxyClient.GetApiVersion();
}
public IdentifiableObjectData[] GetSearchResults(SearchQueryData filter)
{
return ProxyClient.GetSearchResults(filter);
}
public IdentifiableObjectData Read(string id)
{
return ProxyClient.Read(id, new ReadOptions());
}
public ApplicationData ReadApplicationData(string subjectId, string applicationId)
{
return ProxyClient.ReadApplicationData(subjectId, applicationId);
}
public void Dispose()
{
if (ProxyClient.State == CommunicationState.Faulted)
{
ProxyClient.Abort();
}
else
{
ProxyClient.Close();
}
}
}
When you want to perform CRUD actions through the core service you can implement the following methods in the client:
public IdentifiableObjectData CreateItem(IdentifiableObjectData data)
{
data = ProxyClient.Create(data, new ReadOptions());
return data;
}
public IdentifiableObjectData UpdateItem(IdentifiableObjectData data)
{
data = ProxyClient.Update(data, new ReadOptions());
return data;
}
public IdentifiableObjectData ReadItem(string id)
{
return ProxyClient.Read(id, new ReadOptions());
}
To construct a data object of e.g. a Component you can implement a Component Builder class that implements a create method that does this for you:
public ComponentData Create(string folderUri, string title, string content)
{
var data = new ComponentData()
{
Id = "tcm:0-0-0",
Title = title,
Content = content,
LocationInfo = new LocationInfo()
};
data.LocationInfo.OrganizationalItem = new LinkToOrganizationalItemData
{
IdRef = folderUri
};
using (CoreServiceClient client = provider.GetCoreServiceClient())
{
data = (ComponentData)client.CreateItem(data);
}
return data;
}
Hope this gets you started.
In Martin Fowler's "Patterns of Enterprise Application Architecture"
is described approach for organizing DAL like a set of mappers for entities. Each has it's own IdentityMap storing specific entity.
for example in my ASP.NET WebApplication:
//AbstractMapper - superclass for all mappers in DAL
public abstract class AbstractMapper
{
private readonly string _connectionString;
protected string ConnectionString
{
get { return _connectionString; }
}
private readonly DbProviderFactory _dbFactory;
protected DbProviderFactory DBFactory
{
get { return _dbFactory; }
}
#region LoadedObjects (IdentityMap)
protected Hashtable LoadedObjects = new Hashtable();
public void RegisterObject(long id, DomainObject obj)
{
LoadedObjects[id] = obj;
}
public void UnregisterObject(long id)
{
LoadedObjects.Remove(id);
}
#endregion
public AbstractMapper(string connectionString, DbProviderFactory dbFactory)
{
_connectionString = connectionString;
_dbFactory = dbFactory;
}
protected virtual string DBTable
{
get
{
throw new NotImplementedException("database table is not defined in class " + this.GetType());
}
}
protected virtual T Find<T>(long id, IDbTransaction tr = null) where T : DomainObject
{
if (id == 0)
return null;
T result = (T)LoadedObjects[id];
if (result != null)
return result;
IDbConnection cn = GetConnection(tr);
IDbCommand cmd = CreateCommand(GetFindStatement(id), cn, tr);
IDataReader rs = null;
try
{
OpenConnection(cn, tr);
rs = cmd.ExecuteReader(CommandBehavior.SingleRow);
result = (rs.Read()) ? Load<T>(rs) : null;
}
catch (DbException ex)
{
throw new DALException("Error while loading an object by id in class " + this.GetType(), ex);
}
finally
{
CleanUpDBResources(cmd, cn, tr, rs);
}
return result;
}
protected virtual T Load<T>(IDataReader rs) where T : DomainObject
{
long id = GetReaderLong(rs["ID"]);
T result = (T)LoadedObjects[id];
if (result != null)
return result;
result = (T)DoLoad(id, rs);
RegisterObject(id, result);
return result;
}
// another CRUD here ...
}
// Specific Mapper for entity Account
public class AccountMapper : AbstractMapper
{
internal override string DBTable
{
get { return "Account"; }
}
public AccountMapper(string connectionString, DbProviderFactory dbFactory) : base(connectionString, dbFactory) { }
public Account Find(long id)
{
return Find<Account>(id);
}
public override DomainObject DoLoad(long id, IDataReader rs)
{
Account account = new Account(id);
account.Name = GetReaderString(rs["Name"]);
account.Value = GetReaderDecimal(rs["Value"]);
account.CurrencyID = GetReaderLong(rs["CurrencyID"]);
return account;
}
// ...
}
The question is: where to store these mappers? How system services (entities) should call mappers?
I decided to create MapperRegistry containing all mappers. So services can call mappers like:
public class AccountService : DomainService
{
public static Account FindAccount(long accountID)
{
if (accountID > 0)
return MapperRegistry.AccountMapper.Find(accountID);
return null;
}
...
}
But where can I store MapperRegistry instance? I see following variants, but don't like any of them:
MapperRegistry is global for application (Singleton)
Not applicable because of necessity of synchronization in multi-thread ASP.NET application (at least Martin says that only mad can choose this variant)
MapperRegistry per Session
Seems not so good too. All ORMs (NHibernate, LINQ to SQL, EntityFramework) masters advise to use DataContext (NHibernateSession, ObjectContext) per Request and not to store context in Session.
Also in my WebApp almost all requests are AJAX-requests to EntityController.asmx (with attribute ScriptService) returning JSON. And session is not allowed.
MapperRegistry per Request
There are a lot of separate AJAX calls. In this case life cycle of MapperRegistry will be too small. So the data almost always will be retrieved from database, as a result - low performance.
Dear Experts, please help me with architectural solution.