I play a little with handlebars.Net and have a problem with BlockHelper and bindings.
The template (see below) should iterate between persons and only write persons into the result if it´s age is over 18. I think my helper works fine but Handlebars.Net doesn't resolve my binding within the Helper-Block.
I've got this template:
{{#Persons}}
{{gt Age '18'}}
Test
{{Firstname}} {{Lastname}}
{{/gt}}
{{/Persons}}
This is my data:
{
new
{
Firstname = "Luka",
Lastname = "Datrik",
Age = "28"
},
new
{
Firstname = "Max",
Lastname = "Mustermann",
Age = "18"
},
new
{
Firstname = "John",
Lastname = "Doe",
Age = "33"
}
};
The result should be something like:
Test
Luka Datrik
Test
John Doe
But Handlebars doesn´t resolve my binding within the GreaterThan-Block
Is this wanted or a bug?
Here my complete code in C#
static void Main(string[] args)
{
HandlebarsHelper.Register();
var rawFile = File.ReadAllText(".\\TextFile1.txt");
//var content = new XDocument(rawFile).Element("PrintLayout").FirstNode.ToString();
var template = Handlebars.Compile(rawFile);
var result = template(new Dynamic());
File.WriteAllText("temp.txt", result);
System.Diagnostics.Process.Start("temp.txt");
}
Helper:
namespace HandlebarsTests
{
public static class HandlebarsHelper
{
public static readonly new string Equals = "eq";
public static readonly string LowerThan = "lt";
public static readonly string GreaterThan = "gt";
public static readonly string GreaterEquals = "ge";
public static readonly string LowerEquals = "le";
public static readonly string DateFormat = "dateFormat";
public static readonly string FormatStringIndicator = "formatString";
public static void Register()
{
Handlebars.RegisterHelper(DateFormat, (output, context, data) =>
{
if (!DateTime.TryParse(data[0].ToString(), out DateTime date))
{
output.WriteSafeString(data[0].ToString());
return;
}
var dictionary = data[1] as HandlebarsDotNet.Compiler.HashParameterDictionary;
var formatString = dictionary[FormatStringIndicator];
output.WriteSafeString(date.ToString(formatString.ToString()));
});
Handlebars.RegisterHelper(Equals, (output, options, context, data) =>
{
if (data.Length != 2)
options.Inverse(output, null);
if (data[0].Equals(data[1]))
options.Template(output, null);
else
options.Inverse(output, null);
});
Handlebars.RegisterHelper(LowerThan, (output, options, context, data) =>
{
IntegerOperation(LowerThan, ref output, ref options, ref data);
});
Handlebars.RegisterHelper(GreaterThan, (output, options, context, data) =>
{
IntegerOperation(GreaterThan, ref output, ref options, ref data);
});
Handlebars.RegisterHelper(LowerEquals, (output, options, context, data) =>
{
IntegerOperation(LowerEquals, ref output, ref options, ref data);
});
Handlebars.RegisterHelper(GreaterEquals, (output, options, context, data) =>
{
IntegerOperation(GreaterEquals, ref output, ref options, ref data);
});
}
private static void IntegerOperation(string operation, ref System.IO.TextWriter output, ref HelperOptions options, ref object[] data)
{
if (data.Length != 2)
{
options.Inverse(output, null);
return;
}
if (!int.TryParse(data[0].ToString(), out int leftValue))
{
options.Inverse(output, null);
return;
}
if (!int.TryParse(data[1].ToString(), out int rightValue))
{
options.Inverse(output, null);
return;
}
switch (operation)
{
case "lt":
if (leftValue < rightValue)
options.Template(output, null);
else
options.Inverse(output, null);
break;
case "le":
if (leftValue <= rightValue)
options.Template(output, null);
else
options.Inverse(output, null);
break;
case "gt":
if (leftValue > rightValue)
options.Template(output, null);
else
options.Inverse(output, null);
break;
case "ge":
if (leftValue >= rightValue)
options.Template(output, null);
else
options.Inverse(output, null);
break;
default:
break;
}
}
}
I´ve found the solution:
Example for Equals:
Handlebars.RegisterHelper(Equals, (output, options, context, data) =>
{
if (data.Length != 2)
options.Inverse(output, context);
if (data[0].Equals(data[1]))
options.Template(output, context);
else
options.Inverse(output, context);
});
I think it´s a recursive call from options.Template(output, context)
Related
I would like to get the value of Result which comes with invocation.ReturnValue , if it is not async , there is no problem. If the method is async, I cannot get the Result of it
public class RedisCacheAspect: MethodInterception
{
private int _duration;
private IRedisCacheManager _redisCacheManager;
private static readonly ConcurrentDictionary<Type, MethodInfo> TypeofTaskResultMethod = new ConcurrentDictionary<Type, MethodInfo>();
public RedisCacheAspect(int duration = 60)//Default 60
{
_duration = duration;
_redisCacheManager = ServiceTool.ServiceProvider.GetService<IRedisCacheManager>();
}
public override void Intercept(IInvocation invocation)
{
var methodName = string.Format($"{invocation.Method.ReflectedType.FullName}.{invocation.Method.Name}");
var method = invocation.Method;
var arguments = invocation.Arguments.ToList();
var key = KeyGenerator.GetCacheKey(invocation.Method, invocation.Arguments,"FoodThen");
var returnType = invocation.Method.ReturnType;
var isExists = _redisCacheManager.IsAdd(key);
var isAsync = IsAsyncMethod(method);
if (isExists)
{
string cacheValue = GetCacheAsync(key);
var objValue = DeserializeCache(key, cacheValue, returnType);
invocation.ReturnValue = ResultFactory(objValue, returnType, isAsync);
return;
}
invocation.Proceed();
_redisCacheManager.Set(key, invocation.ReturnValue,TimeSpan.FromMinutes(_duration));
}
DeserializeCache Method :
private object DeserializeCache(string cacheKey, string cacheValue, Type returnType)
{
try
{
return JsonConvert.DeserializeObject(cacheValue, returnType);
}
catch (System.Exception)
{
_redisCacheManager.Remove(cacheKey);
return null;
}
}
ResultFactory Method :
private object ResultFactory(object result, Type returnType, bool isAsync)
{
if (isAsync)
{
return TypeofTaskResultMethod
.GetOrAdd(returnType, t => typeof(Task)
.GetMethods()
.First(p => p.Name == "FromResult" && p.ContainsGenericParameters)
.MakeGenericMethod(returnType))
.Invoke(null, new object[] { result });
}
else
{
return result;
}
}
This is how invocation.ReturnValue looks like and I want to get the value of Result...
Since you've debugged,have you noticed the value of returntype after var returnType = invocation.Method.ReturnType; been executed?
For Async methods ,the return type should be:
invocation.Method.ReturnType.GenericTypeArguments.First();
In the Book "Introduction to Vala" by Dr Michael Lauer, he has mentioned that the lib Soup async api is broken. I'm struggling to write a simple example using session.queue_message that query radio stations using the service from radio-browser. Here is my code. I would appreciate any help form experienced Programmers like "Al Thomas". Thank you.
public class Station : Object {
// A globally unique identifier for the change of the station information
public string changeuuid { get; set; default = ""; }
// A globally unique identifier for the station
public string stationuuid { get; set; default = ""; }
// The name of the station
public string name { get; set; default = ""; }
// The stream URL provided by the user
public string url { get; set; default = ""; }
// and so on ... many properties
public string to_string () {
var builder = new StringBuilder ();
builder.append_printf ("\nchangeuuid = %s\n", changeuuid);
builder.append_printf ("stationuuid = %s\n", stationuuid);
builder.append_printf ("name = %s\n", name);
builder.append_printf ("url = %s\n", url);
return (owned) builder.str;
}
}
public class RadioBrowser : Object {
private static Soup.Session session;
// private static MainLoop main_loop;
public const string API_URL = "https://de1.api.radio-browser.info/json/stations";
public const string USER_AGENT = "github.aeldemery.radiolibrary";
public RadioBrowser (string user_agent = USER_AGENT, uint timeout = 50)
requires (timeout > 0)
{
Intl.setlocale ();
session = new Soup.Session ();
session.timeout = timeout;
session.user_agent = user_agent;
session.use_thread_context = true;
// main_loop = new MainLoop ();
}
private void check_response_status (Soup.Message msg) {
if (msg.status_code != 200) {
var str = "Error: Status message error %s.".printf (msg.reason_phrase);
error (str);
}
}
public Gee.ArrayList<Station> listStations () {
var stations = new Gee.ArrayList<Station> ();
var data_list = Datalist<string> ();
data_list.set_data ("limit", "100");
var parser = new Json.Parser ();
parser.array_element.connect ((pars, array, index) => {
var station = Json.gobject_deserialize (typeof (Station), array.get_element (index)) as Station;
assert_nonnull (station);
stations.add (station);
});
var msg = Soup.Form.request_new_from_datalist (
"POST",
API_URL,
data_list
);
// send_message works but not queue_message
// session.send_message (msg);
session.queue_message (msg, (sess, mess) => {
check_response_status (msg);
try {
parser.load_from_data ((string) msg.response_body.flatten ().data);
} catch (Error e) {
error ("Failed to parse data, error:" + e.message);
}
});
return stations;
}
}
int main (string[] args) {
var radio_browser = new RadioBrowser ();
var stations = radio_browser.listStations ();
assert_nonnull (stations);
foreach (var station in stations) {
print (station.to_string ());
}
return 0;
}
While I'm not Al Thomas, I still might be able to help. ;)
For async calls to work in there needs to be a main loop running, and typically from the main program thread. Thus you want to create and execute the main loop from your main() function, rather than in your application code:
int main (string[] args) {
var loop = new GLib.MainLoop ();
var radio_browser = new RadioBrowser ();
// set up async calls here
// then set the main loop running as the last thing
loop.run();
}
Also, if you want to wait for an async call to complete, you typically need to make the call using the yield keyword from another async function E.g:
public async Gee.ArrayList<Station> listStations () {
…
// When this call is made, execution of listStations() will be
// suspended until the soup response is received
yield session.send_async(msg);
// Execution then resumes normally
check_response_status (msg);
parser.load_from_data ((string) msg.response_body.flatten ().data);
…
return stations;
}
You can then call this from the (non-async) main function using the listStations.begin(…) notation:
int main (string[] args) {
var loop = new GLib.MainLoop ();
var radio_browser = new RadioBrowser ();
radio_browser.listStations.begin((obj, res) => {
var stations = radio_browser.listStations.end(res);
…
loop.quit();
});
loop.run();
}
As further reading, I would recommend the async section of the Vala Tutorial, and the asyc examples on the wiki as well.
Json.NET seems to handle converting many names which would be invalid XML correctly, for example:
JsonConvert.DeserializeXmlNode("{"name!1": "test"}").OuterXml
Results in:
<name_x0021_1>test</name_x0021_1>
But attempting to convert the following, which passes JSONLint:
{"$": "test"}
Results in
Result Message: System.Xml.XmlException : The ':' character, hexadecimal value 0x3A, cannot be included in a name.
This error message in itself seems confusing as it suggests one of the names in the JSON has a : character. There might be a good reason for this but is there a way to get this to convert to XML without throwing an exception as some APIs seem to return the "$": "...." pair.
Update
This is fixed in Json.NET 9.0.1 in commit b71ca75.
Original Answer
This may be a bug in Json.NET's XmlNodeConverter.ReadElement(). Json.NET has several reserved property names, all of which begin with $, shown here:
public const string IdPropertyName = "$id";
public const string RefPropertyName = "$ref";
public const string TypePropertyName = "$type";
public const string ValuePropertyName = "$value";
public const string ArrayValuesPropertyName = "$values";
Then, in ReadElement(), there is special handling for property names that begin with a $:
private void ReadElement(JsonReader reader, IXmlDocument document, IXmlNode currentNode, string propertyName, XmlNamespaceManager manager)
{
if (string.IsNullOrEmpty(propertyName))
{
throw JsonSerializationException.Create(reader, "XmlNodeConverter cannot convert JSON with an empty property name to XML.");
}
Dictionary<string, string> attributeNameValues = ReadAttributeElements(reader, manager);
string elementPrefix = MiscellaneousUtils.GetPrefix(propertyName);
if (propertyName.StartsWith('#'))
{
string attributeName = propertyName.Substring(1);
string attributePrefix = MiscellaneousUtils.GetPrefix(attributeName);
AddAttribute(reader, document, currentNode, attributeName, manager, attributePrefix);
}
else if (propertyName.StartsWith('$'))
{
if (propertyName == JsonTypeReflector.ArrayValuesPropertyName)
{
propertyName = propertyName.Substring(1);
elementPrefix = manager.LookupPrefix(JsonNamespaceUri);
CreateElement(reader, document, currentNode, propertyName, manager, elementPrefix, attributeNameValues);
}
else
{
// Your code throws an exception going down this branch.
string attributeName = propertyName.Substring(1);
string attributePrefix = manager.LookupPrefix(JsonNamespaceUri);
AddAttribute(reader, document, currentNode, attributeName, manager, attributePrefix);
}
}
else
{
CreateElement(reader, document, currentNode, propertyName, manager, elementPrefix, attributeNameValues);
}
}
I suspect the propertyName.StartsWith('$') branch should only be taken for reserved property names, meaning the method should look like:
private void ReadElement(JsonReader reader, IXmlDocument document, IXmlNode currentNode, string propertyName, XmlNamespaceManager manager)
{
if (string.IsNullOrEmpty(propertyName))
{
throw JsonSerializationException.Create(reader, "XmlNodeConverter cannot convert JSON with an empty property name to XML.");
}
Dictionary<string, string> attributeNameValues = ReadAttributeElements(reader, manager);
if (propertyName.StartsWith('#'))
{
string attributeName = propertyName.Substring(1);
string attributePrefix = MiscellaneousUtils.GetPrefix(attributeName);
AddAttribute(reader, document, currentNode, attributeName, manager, attributePrefix);
}
else if (propertyName == JsonTypeReflector.ArrayValuesPropertyName)
{
propertyName = propertyName.Substring(1);
var elementPrefix = manager.LookupPrefix(JsonNamespaceUri);
CreateElement(reader, document, currentNode, propertyName, manager, elementPrefix, attributeNameValues);
}
else if (propertyName == JsonTypeReflector.IdPropertyName
|| propertyName == JsonTypeReflector.RefPropertyName
|| propertyName == JsonTypeReflector.TypePropertyName
|| propertyName == JsonTypeReflector.ValuePropertyName)
{
string attributeName = propertyName.Substring(1);
string attributePrefix = manager.LookupPrefix(JsonNamespaceUri);
AddAttribute(reader, document, currentNode, attributeName, manager, attributePrefix);
}
else
{
var elementPrefix = MiscellaneousUtils.GetPrefix(propertyName);
CreateElement(reader, document, currentNode, propertyName, manager, elementPrefix, attributeNameValues);
}
}
(Honestly, the handling of "$value" looks suspect also, since the value of a polymorphic property could be any JSON object.)
You might want to report an issue.
In the meantime, you could load your JSON into a JToken, manually remap names beginning with $, then convert to an XmlDocument like so:
var token = JToken.Parse(json);
token.RenameReplaceProperties(s => (s.StartsWith("$") && !JsonExtensions.IsReserved(s) ? XmlConvert.EncodeName(s) : s));
var xml = token.ToXmlNode();
Using the extension methods:
public static class JsonExtensions
{
const string IdPropertyName = "$id";
const string RefPropertyName = "$ref";
const string TypePropertyName = "$type";
const string ValuePropertyName = "$value";
const string ArrayValuesPropertyName = "$values";
public static bool IsReserved(string s)
{
return s == IdPropertyName || s == RefPropertyName || s == TypePropertyName || s == ValuePropertyName || s == ArrayValuesPropertyName;
}
public static IEnumerable<JToken> DescendantsAndSelf(this JToken root)
{
var container = root as JContainer;
if (container != null)
return container.DescendantsAndSelf();
else if (root != null)
return new[] { root };
else
return Enumerable.Empty<JToken>();
}
public static JProperty RenameReplace(this JProperty property, string name)
{
if (property == null || name == null)
throw new ArgumentNullException();
var value = property.Value;
property.Value = null;
var newProperty = new JProperty(name, value);
property.Replace(newProperty);
return newProperty;
}
public static JToken RenameReplaceProperties(this JToken root, Func<string, string> map)
{
var query = from property in root.DescendantsAndSelf().OfType<JProperty>()
let name = map(property.Name)
where name != property.Name && name != null
select new KeyValuePair<JProperty, string>(property, name);
foreach (var pair in query.ToList())
{
var newProperty = pair.Key.RenameReplace(pair.Value);
if (pair.Key == root)
root = newProperty;
}
return root;
}
public static XmlDocument ToXmlNode(this JToken root)
{
using (var reader = root.CreateReader())
return DeserializeXmlNode(reader);
}
public static XmlDocument DeserializeXmlNode(JsonReader reader)
{
return DeserializeXmlNode(reader, null, false);
}
public static XmlDocument DeserializeXmlNode(JsonReader reader, string deserializeRootElementName, bool writeArrayAttribute)
{
var converter = new Newtonsoft.Json.Converters.XmlNodeConverter() { DeserializeRootElementName = deserializeRootElementName, WriteArrayAttribute = writeArrayAttribute };
var jsonSerializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { Converters = new JsonConverter[] { converter } });
return (XmlDocument)jsonSerializer.Deserialize(reader, typeof(XmlDocument));
}
}
My stored procedure is returning dynamic pivot columns as available. I am using the SQLMapper and COlumnTypeAttribute, But in the results I can only see the first column and its value, but the dynamic pivot column(s) and their values(s) are empty.
the sample data could look like
The first column is fixed, Rest of the columns are pivot columns.
TSBNumber SystemXY SystemBB SystemTT
asdas 1/1/2013
1231 1/1/2014
12312 1/1/2013
ASAWS 1/1/2013
awsdS 1/1/2013
Store Procedure
DECLARE #PivotColumnHeaders NVARCHAR(MAX)
SELECT #PivotColumnHeaders =
COALESCE(
#PivotColumnHeaders + ',[' + cast(SystemFullName as Nvarchar) + ']',
'[' + cast(SystemFullName as varchar)+ ']'
)
FROM System
WHERE (#SelectedSystemIDs IS NULL OR System.ID IN(select * from dbo.SplitInts_RBAR_1(#SelectedSystemIDs, ',')))
AND ((#PlatformID IS NULL) OR (System.PlatformID = #PlatformID) OR (#PlatformID = 12 AND System.PlatformID <= 2))
DECLARE #PivotTableSQL NVARCHAR(MAX)
SET #PivotTableSQL = N'
SELECT *
FROM (
SELECT
TSBNumber [TSBNumber],
SystemFullName,
ClosedDate
FROM ServiceEntry
INNER JOIN System
ON ServiceEntry.SystemID = System.ID
where
(ServiceEntry.TSBNumber IS NOT NULL)
AND
(ServiceEntry.ClosedDate IS NOT NULL)
AND
(
(''' + #SelectedTsbIDs + ''' = '''+ '0' + ''') OR
(ServiceEntry.TSBNumber in (select * from dbo.SplitStrings_Moden(''' + #SelectedTsbIDs + ''', ''' + ',' + ''')))
)
) AS PivotData
PIVOT (
MAX(ClosedDate)
FOR SystemFullName IN (
' + #PivotColumnHeaders + '
)
) AS PivotTable
'
EXECUTE (#PivotTableSQL)
ColumnAttributeTypeManager
namespace RunLog.Domain.Exports
{
/// <summary>
/// Uses the Name value of the ColumnAttribute specified, otherwise maps as usual.
/// </summary>
/// <typeparam name="T">The type of the object that this mapper applies to.</typeparam>
public class ColumnAttributeTypeMapper<T> : FallbackTypeMapper
{
public static readonly string ColumnAttributeName = "ColumnAttribute";
public ColumnAttributeTypeMapper()
: base(new SqlMapper.ITypeMap[]
{
new CustomPropertyTypeMap(
typeof(T),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName)
)
),
new DefaultTypeMap(typeof(T))
})
{
}
//public ColumnAttributeTypeMapper()
// : base(new SqlMapper.ITypeMap[]
// {
// new CustomPropertyTypeMap(typeof (T), SelectProperty),
// new DefaultTypeMap(typeof (T))
// })
//{
//}
//private static PropertyInfo SelectProperty(Type type, string columnName)
//{
// return
// type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).
// FirstOrDefault(
// prop =>
// prop.GetCustomAttributes(false)
// // Search properties to find the one ColumnAttribute applied with Name property set as columnName to be Mapped
// .Any(attr => attr.GetType().Name == ColumnAttributeName
// &&
// attr.GetType().GetProperties(BindingFlags.Public |
// BindingFlags.NonPublic |
// BindingFlags.Instance)
// .Any(
// f =>
// f.Name == "Name" &&
// f.GetValue(attr).ToString().ToLower() == columnName.ToLower()))
// && // Also ensure the property is not read-only
// (prop.DeclaringType == type
// ? prop.GetSetMethod(true)
// : prop.DeclaringType.GetProperty(prop.Name,
// BindingFlags.Public | BindingFlags.NonPublic |
// BindingFlags.Instance).GetSetMethod(true)) != null
// );
//}
}
public class MyModel
{
[Column("TSBNumber")]
public string TSBNumber { get; set; }
[Column(Name = "FileKey")]
public string MyProperty2 { get; set; }
//public string MyProperty2 { get; set; } // Uses Default Mapping
// ...
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class ColumnAttribute : Attribute
{
public string Name { get; set; }
public ColumnAttribute() { }
public ColumnAttribute(string Name) { this.Name = Name; }
}
public class FallbackTypeMapper : SqlMapper.ITypeMap
{
private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;
public FallbackTypeMapper(IEnumerable<SqlMapper.ITypeMap> mappers)
{
_mappers = mappers;
}
public ConstructorInfo FindConstructor(string[] names, Type[] types)
{
foreach (var mapper in _mappers)
{
try
{
ConstructorInfo result = mapper.FindConstructor(names, types);
if (result != null)
{
return result;
}
}
catch (NotImplementedException)
{
}
}
return null;
}
public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
{
foreach (var mapper in _mappers)
{
try
{
var result = mapper.GetConstructorParameter(constructor, columnName);
if (result != null)
{
return result;
}
}
catch (NotImplementedException)
{
}
}
return null;
}
public SqlMapper.IMemberMap GetMember(string columnName)
{
foreach (var mapper in _mappers)
{
try
{
var result = mapper.GetMember(columnName);
if (result != null)
{
return result;
}
}
catch (NotImplementedException)
{
}
}
return null;
}
}
}
Executing Stored Procedure
public static string ServiceTsbExport(DateTime StartDate, DateTime EndDate, int UserRoleID,
string SelectedSystemIDs,
string SelectedTsbIDs)
{
EFDbContext db = new EFDbContext();
Dapper.SqlMapper.SetTypeMap(typeof(MyModel), new ColumnAttributeTypeMapper<MyModel>());
return db.Database.SqlQuery<MyModel>("[dbo].[spExportServiceTSB] #parm1, #parm2, #parm3, #parm4, #parm5",
new SqlParameter("parm1", StartDate),
new SqlParameter("parm2", EndDate),
new SqlParameter("parm3", SelectedSystemIDs),
new SqlParameter("parm4", SelectedTsbIDs),
new SqlParameter("parm5", UserRoleID)
).ToList().ToHTMLTable();
}
I think you're making this very hard while it could be simple. I've once done almost exactly the same thing. Here's an anonymized version of it:
using Dapper;
...
using (var cnn = new SqlConnection(#"Data Source=... etc."))
{
cnn.Open();
var p = new DynamicParameters();
p.Add("#params", "Id=21");
var obs = cnn.Query(sql:"GetPivotData", param: p,
commandType:CommandType.StoredProcedure);
var dt = ToDataTable(obs);
}
This ToDataTable method is probably similar to your ToHTMLTable method. Here it is:
public DataTable ToDataTable(IEnumerable<dynamic> items)
{
if (items == null) return null;
var data = items.ToArray();
if (data.Length == 0) return null;
var dt = new DataTable();
foreach(var pair in ((IDictionary<string, object>)data[0]))
{
dt.Columns.Add(pair.Key, (pair.Value ?? string.Empty).GetType());
}
foreach (var d in data)
{
dt.Rows.Add(((IDictionary<string, object>)d).Values.ToArray());
}
return dt;
}
The heart of the logic is that the dynamic returned by Dapper's Query() extension method can be cast to an IDictionary<string, object>.
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