Skip to content

Instantly share code, notes, and snippets.

@bboyle1234
Last active February 27, 2018 03:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bboyle1234/46291a8c8d42f797405057844eeb4bda to your computer and use it in GitHub Desktop.
Save bboyle1234/46291a8c8d42f797405057844eeb4bda to your computer and use it in GitHub Desktop.
IMigratable
using Newtonsoft.Json;
namespace Migratable {
[JsonConverter(typeof(MigratableConverter))]
public interface IMigratable {
}
}
using System;
namespace Migratable {
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class MigratableAttribute : Attribute {
public readonly Guid Id;
public MigratableAttribute(string guid) {
Id = Guid.Parse(guid);
}
}
}
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Apex.CQRSOrleans.Migratable {
public class MigratableConverter : JsonConverter {
[ThreadStatic]
static bool writeDisabled = false;
[ThreadStatic]
static bool readDisabled = false;
public override bool CanRead => !readDisabled;
public override bool CanWrite => !writeDisabled;
public override bool CanConvert(Type objectType) => typeof(IMigratable).IsAssignableFrom(objectType);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
try {
writeDisabled = true;
if (null == value) {
writer.WriteValue(value);
} else {
var jObject = JObject.FromObject(value);
jObject.Add("$typeId", MigratableTypes.GetTypeId(value.GetType()));
jObject.WriteTo(writer);
}
} finally {
writeDisabled = false;
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
try {
readDisabled = true;
var jObject = JToken.ReadFrom(reader) as JObject;
if (null == jObject) return null;
var typeId = (Guid)jObject.GetValue("$typeId");
var type = MigratableTypes.GetType(typeId);
return JsonConvert.DeserializeObject(jObject.ToString(), type);
} finally {
readDisabled = false;
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace Apex.CQRSOrleans.Migratable {
[Serializable]
public class MigratableTypeNotFoundException : Exception {
public readonly Guid TypeId;
public MigratableTypeNotFoundException(Guid typeId) : this(typeId, null) { }
public MigratableTypeNotFoundException(Guid typeId, Exception innerException) : base($"Migratable type with id '{typeId:N}' not found in any loaded assembly.", innerException) => TypeId = typeId;
public MigratableTypeNotFoundException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
#if NETCOREAPP2_0 || NETSTANDARD2_0
using Microsoft.Extensions.DependencyModel;
#endif
namespace Apex.CQRSOrleans.Migratable {
public static class MigratableTypes {
static readonly Dictionary<Guid, Type> Data = new Dictionary<Guid, Type>();
static MigratableTypes() {
foreach (var type in GetIMigratableTypes()) {
CheckIMigratableRules(type);
Data[GetTypeId(type)] = type;
}
}
static IEnumerable<Type> GetIMigratableTypes() {
#if NETCOREAPP2_0 || NETSTANDARD2_0
//http://www.michael-whelan.net/replacing-appdomain-in-dotnet-core/
return DependencyContext.Default.CompileLibraries.Select(l => l.Name)
.Where(s => !s.StartsWith("Microsoft."))
.Where(s => !s.StartsWith("System."))
.Where(s => !s.StartsWith("runtime."))
.OrderBy(s => s)
.SelectMany(assemblyName => {
try {
return Assembly.Load(assemblyName).DefinedTypes
.Where(t => !t.IsAbstract && typeof(IMigratable).IsAssignableFrom(t))
.Select(t => t.AsType());
} catch { // Some assemblies don't always play ball with loading. If we ever run into a point where one of them is needed, highly unlikely, we can look at improving this method.
return new Type[0];
}
});
#elif NET461
return AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes()
.Where(t => typeof(IMigratable).IsAssignableFrom(t))
.Where(t => !t.IsAbstract));
#endif
}
static void CheckIMigratableRules(Type type) {
// Check for duplicate IMigratable identifiers
var id = GetTypeId(type);
if (Data.ContainsKey(id))
throw new MigratableTypeValidationException($"Duplicate '{nameof(MigratableAttribute)}' value found on types '{type.FullName}' and '{Data[id].FullName}'.");
// [DataContract] attribute is required, on EVERY class, not just base classes
if (type.GetCustomAttributes(typeof(DataContractAttribute), false).Length == 0)
throw new MigratableTypeValidationException($"'{nameof(IMigratable)}' objects are required to use the '[DataContract]' attribute. Class: '{type.FullName}'.");
// Collect information about [DataMember] attributes on all fields and properties including inherited and private.
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
var fields = type.GetFields(bindingFlags).Where(f => null != f.GetCustomAttribute(typeof(DataMemberAttribute))).ToArray();
var properties = type.GetProperties(bindingFlags).Where(p => null != p.GetCustomAttribute(typeof(DataMemberAttribute))).ToArray();
var members = fields.Cast<MemberInfo>().Concat(properties.Cast<MemberInfo>())
.Select(m => new {
Member = m,
DataMemberAttribute = (DataMemberAttribute)m.GetCustomAttribute(typeof(DataMemberAttribute))
}).ToArray();
// Check that DataMember names are explicitly set eg [DataMember(Name = "xx")]
var noName = members.FirstOrDefault(m => !m.DataMemberAttribute.IsNameSetExplicitly);
if (null != noName) {
var message = $"'{nameof(IMigratable)}' objects are required to set DataMember names explicitly. Class: '{type.FullName}', Field: '{noName.Member.Name}'.";
throw new MigratableTypeValidationException(message);
}
// Check that DataMember names are not accidentally duplicated.
var duplicateName = members.GroupBy(m => m.DataMemberAttribute.Name).FirstOrDefault(g => g.Count() > 1);
if (null != duplicateName) {
throw new MigratableTypeValidationException($"Duplicate DataMemberName '{duplicateName.Key}' found on class '{type.FullName}'.");
}
}
public static Type GetType(Guid typeId) {
try { return Data[typeId]; } catch (KeyNotFoundException x) { throw new MigratableTypeNotFoundException(typeId, x); }
}
public static Guid GetTypeId(Type type) {
var a = type.GetCustomAttributes(typeof(MigratableAttribute), false)
.Cast<MigratableAttribute>()
.FirstOrDefault();
if (null == a)
throw new MigratableTypeValidationException($"'{nameof(MigratableAttribute)}' attribute does not exist on type '{type.FullName}'.");
if (Guid.Empty == a.Id)
throw new MigratableTypeValidationException($"'{nameof(MigratableAttribute)}' attribute was not set to a proper value on type '{type.FullName}'.");
return a.Id;
}
public static IEnumerable<KeyValuePair<Guid, Type>> GetMigratableTypes() {
return Data;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace Apex.CQRSOrleans.Migratable {
[Serializable]
public class MigratableTypeValidationException : Exception {
public MigratableTypeValidationException(String message) : base(message) { }
public MigratableTypeValidationException(String message, Exception innerException) : base(message, innerException) { }
public MigratableTypeValidationException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment