public FieldInfo[] GetAllFieldsThatShouldBeSet(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type)); } var fields = new List <FieldInfo>(); var currentType = type; while (currentType != null) { foreach (var field in currentType.GetFields(BinaryReaderWriterShared.MemberRetrievalBindingFlags)) { if (!BinaryReaderWriterShared.IgnoreField(field) && (field.GetCustomAttribute <OptionalWhenDeserialisingAttribute>() == null) && (BackingFieldHelpers.TryToGetPropertyRelatingToBackingField(field)?.GetCustomAttribute <OptionalWhenDeserialisingAttribute>() == null)) { fields.Add(field); } } currentType = currentType.BaseType; } return(fields.ToArray()); }
public static void AllowDeserialisationIfFieldCanNotBeSetIfFieldIsForAutoPropertyThatIsMarkedAsOptionalForDeserialisation() { var sourceType = ConstructType(GetModuleBuilder("DynamicAssemblyFor" + GetMyName(), new Version(1, 0)), "MyClass", new Tuple <string, Type> [0]); var instance = Activator.CreateInstance(sourceType); var serialisedData = BinarySerialisation.Serialise(instance); const string namePropertyName = "Name"; var destinationType = ConstructType( GetModuleBuilder("DynamicAssemblyFor" + GetMyName(), new Version(1, 0)), "MyClass", fields: new Tuple <string, Type> [0], optionalFinisher: typeBuilder => { // Need to define the field using this lambda rather than specifying it through the fields argument because we need the reference for use in the property getter var fieldBuilder = typeBuilder.DefineField(BackingFieldHelpers.GetBackingFieldName(namePropertyName), typeof(string), FieldAttributes.Private); var propertyBuilder = typeBuilder.DefineProperty(namePropertyName, PropertyAttributes.None, typeof(string), parameterTypes: Type.EmptyTypes); propertyBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(DeprecatedAttribute).GetConstructor(new[] { typeof(string) }), new object[] { null })); var getterBuilder = typeBuilder.DefineMethod("get_" + namePropertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(string), parameterTypes: Type.EmptyTypes); var ilGenerator = getterBuilder.GetILGenerator(); ilGenerator.Emit(OpCodes.Ldarg_0); ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder); ilGenerator.Emit(OpCodes.Ret); propertyBuilder.SetGetMethod(getterBuilder); propertyBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(OptionalWhenDeserialisingAttribute).GetConstructor(Type.EmptyTypes), new object[0])); } ); var clone = ResolveDynamicAssembliesWhilePerformingAction( () => Deserialise(serialisedData, destinationType), destinationType ); var namePropertyOnDestination = destinationType.GetProperty(namePropertyName); Assert.Null(namePropertyOnDestination.GetValue(clone)); }
public static void DeprecatedPropertyShouldBeSerialisedAsIfItIsAnAutoProperty() { // In a real world scenario, the "future type" would have other properties and the [Deprecated] one(s) would have values computed from them.. for the interests of this // unit test, the future type (typeWithDeprecatedProperty) will ONLY have a [Deprecated] property that has a "computed value" (ie. a getter that always returns 123) const string idPropertyName = "Id"; const int hardCodedIdValueForDeprecatedProperty = 123; var typeWithDeprecatedProperty = ConstructType( GetModuleBuilder("DynamicAssemblyFor" + GetMyName(), new Version(1, 0)), "MyClass", fields: new Tuple <string, Type> [0], optionalFinisher: typeBuilder => { var propertyBuilder = typeBuilder.DefineProperty(idPropertyName, PropertyAttributes.None, typeof(int), parameterTypes: Type.EmptyTypes); propertyBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(DeprecatedAttribute).GetConstructor(new[] { typeof(string) }), new object[] { null })); var getterBuilder = typeBuilder.DefineMethod("get_" + idPropertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(int), parameterTypes: Type.EmptyTypes); var ilGenerator = getterBuilder.GetILGenerator(); ilGenerator.Emit(OpCodes.Ldc_I4, hardCodedIdValueForDeprecatedProperty); ilGenerator.Emit(OpCodes.Ret); propertyBuilder.SetGetMethod(getterBuilder); } ); var instance = Activator.CreateInstance(typeWithDeprecatedProperty); var serialisedData = BinarySerialisation.Serialise(instance); // For the sake of this test, the type that will be deserialised to only needs to have a backing field for an auto property (the field that we're expecting to // get set) and so that's all that is being configured. It would be closer to a real use case if there was a property with a getter that used this backing field // but it wouldn't be used by this test and so it doesn't need to be created. var idBackingFieldName = BackingFieldHelpers.GetBackingFieldName(idPropertyName); var typeThatStillHasThePropertyOnIt = ConstructType( GetModuleBuilder("DynamicAssemblyFor" + GetMyName(), new Version(1, 0)), "MyClass", new[] { Tuple.Create(idBackingFieldName, typeof(int)) } ); var deserialised = ResolveDynamicAssembliesWhilePerformingAction( () => Deserialise(serialisedData, typeThatStillHasThePropertyOnIt), typeThatStillHasThePropertyOnIt ); var idBackingFieldOnDestination = typeThatStillHasThePropertyOnIt.GetField(idBackingFieldName); Assert.Equal(123, idBackingFieldOnDestination.GetValue(deserialised)); }
public static void DeprecatedPropertyIsWrittenAgainstOldPropertyNameAsWellAsNew() { // Define two versions of the same class where the V1 looks like this: // // public class SomethingWithName // { // public string NameOld { get; set; } // } // // .. and the V2 looks like this: // // public class SomethingWithName // { // public string NameNew { get; set; } // // [Deprecated] // public string NameOld { get { return NameNew; } } // } // // If an instance of V2 entity is serialised and then deserialised into the V1 entity type then the "NameOld" field of the V1 entity instance should be populated (in order for // this to work, the member-setter-generation logic needs to consider fields and properties and the attributes that they do or do not have) var entityTypeV1 = ConstructType( GetModuleBuilder("DynamicAssemblyFor" + GetMyName(), new Version(1, 0)), "SomethingWithName", fields: new Tuple <string, Type> [0], optionalFinisher: typeBuilder => { // Backing field for "NameOld" var nameOldfieldBuilder = typeBuilder.DefineField(BackingFieldHelpers.GetBackingFieldName("NameOld"), typeof(string), FieldAttributes.Private); // Property for "NameOld" var nameOldPropertyBuilder = typeBuilder.DefineProperty("NameOld", PropertyAttributes.None, typeof(string), parameterTypes: Type.EmptyTypes); var nameOldGetterBuilder = typeBuilder.DefineMethod("get_NameOld", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(string), parameterTypes: Type.EmptyTypes); var ilGeneratorForNameOldGetter = nameOldGetterBuilder.GetILGenerator(); ilGeneratorForNameOldGetter.Emit(OpCodes.Ldarg_0); ilGeneratorForNameOldGetter.Emit(OpCodes.Ldfld, nameOldfieldBuilder); ilGeneratorForNameOldGetter.Emit(OpCodes.Ret); nameOldPropertyBuilder.SetGetMethod(nameOldGetterBuilder); var nameOldSetterBuilder = typeBuilder.DefineMethod("set_NameOld", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(void), parameterTypes: new[] { typeof(string) }); var ilGeneratorForNameOldSetter = nameOldSetterBuilder.GetILGenerator(); ilGeneratorForNameOldSetter.Emit(OpCodes.Ldarg_0); ilGeneratorForNameOldSetter.Emit(OpCodes.Ldarg_1); ilGeneratorForNameOldSetter.Emit(OpCodes.Stfld, nameOldfieldBuilder); ilGeneratorForNameOldSetter.Emit(OpCodes.Ret); nameOldPropertyBuilder.SetSetMethod(nameOldSetterBuilder); } ); var entityTypeV2 = ConstructType( GetModuleBuilder("DynamicAssemblyFor" + GetMyName(), new Version(1, 0)), "SomethingWithName", fields: new Tuple <string, Type> [0], optionalFinisher: typeBuilder => { // Backing field for "NameNew" var nameNewfieldBuilder = typeBuilder.DefineField(BackingFieldHelpers.GetBackingFieldName("NameNew"), typeof(string), FieldAttributes.Private); // Property for "NameNew" var nameNewPropertyBuilder = typeBuilder.DefineProperty("NameNew", PropertyAttributes.None, typeof(string), parameterTypes: Type.EmptyTypes); var nameNewGetterBuilder = typeBuilder.DefineMethod("get_NameNew", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(string), parameterTypes: Type.EmptyTypes); var ilGeneratorForNameNewGetter = nameNewGetterBuilder.GetILGenerator(); ilGeneratorForNameNewGetter.Emit(OpCodes.Ldarg_0); ilGeneratorForNameNewGetter.Emit(OpCodes.Ldfld, nameNewfieldBuilder); ilGeneratorForNameNewGetter.Emit(OpCodes.Ret); nameNewPropertyBuilder.SetGetMethod(nameNewGetterBuilder); var nameNewSetterBuilder = typeBuilder.DefineMethod("set_NameNew", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(void), parameterTypes: new[] { typeof(string) }); var ilGeneratorForNameNewSetter = nameNewSetterBuilder.GetILGenerator(); ilGeneratorForNameNewSetter.Emit(OpCodes.Ldarg_0); ilGeneratorForNameNewSetter.Emit(OpCodes.Ldarg_1); ilGeneratorForNameNewSetter.Emit(OpCodes.Stfld, nameNewfieldBuilder); ilGeneratorForNameNewSetter.Emit(OpCodes.Ret); nameNewPropertyBuilder.SetSetMethod(nameNewSetterBuilder); // Property for "NameOld" that has [Deprecated] attribute and whose getter access "NameNew" var nameOldPropertyBuilder = typeBuilder.DefineProperty("NameOld", PropertyAttributes.None, typeof(string), parameterTypes: Type.EmptyTypes); var nameOldGetterBuilder = typeBuilder.DefineMethod("get_NameOld", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(string), parameterTypes: Type.EmptyTypes); var ilGeneratorForNameOldGetter = nameOldGetterBuilder.GetILGenerator(); ilGeneratorForNameOldGetter.Emit(OpCodes.Ldarg_0); ilGeneratorForNameOldGetter.Emit(OpCodes.Call, nameNewGetterBuilder); ilGeneratorForNameOldGetter.Emit(OpCodes.Ret); nameOldPropertyBuilder.SetGetMethod(nameOldGetterBuilder); nameOldPropertyBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeof(DeprecatedAttribute).GetConstructor(new[] { typeof(string) }), new object[] { null })); } ); // Create an instance of the V2 entity var source = Activator.CreateInstance(entityTypeV2); entityTypeV2.GetProperty("NameNew").SetValue(source, "abc"); // Try to create a member setter for it - this should work since it only has string fields and properties var memberSetterDetails = GetMemberSetterAvailability( entityTypeV2, DefaultTypeAnalyser.Instance, valueWriterRetriever: t => null // No complex nested member setter madness required, so provide a valueWriterRetriever delegate that always returns null ) .MemberSetterDetailsIfSuccessful; Assert.NotNull(memberSetterDetails); // Serialise this v2 entity instance byte[] serialised; using (var stream = new MemoryStream()) { foreach (var fieldName in memberSetterDetails.FieldsSet) { var fieldNameBytes = new[] { (byte)BinarySerialisationDataType.FieldNamePreLoad }.Concat(fieldName.AsStringAndReferenceID).ToArray(); stream.Write(fieldNameBytes, 0, fieldNameBytes.Length); } var writer = new BinarySerialisationWriter(stream); writer.ObjectStart(source.GetType()); memberSetterDetails.GetCompiledMemberSetter()(source, writer); writer.ObjectEnd(); serialised = stream.ToArray(); } // Ensure that it works deserialising back to an older version of the type var deserialisedAsV1 = ResolveDynamicAssembliesWhilePerformingAction( () => BinarySerialisation.Deserialise <object>(serialised), entityTypeV1 ); Assert.NotNull(deserialisedAsV1); Assert.IsType(entityTypeV1, deserialisedAsV1); Assert.Equal("abc", deserialisedAsV1.GetType().GetProperty("NameOld").GetValue(deserialisedAsV1)); }
/// <summary> /// There may be cases where a value is deserialised for a field that does not exist on the destination type - this could happen if data from an old version of the type is /// being deserialised into a new version of the type, in which case we need to check for any [Deprecated(replacedBy: ..)] properties that exist to provide a way to take /// this value and set the [Deprecated] property/ies using the field that no longer exists. Note that it's also possible that the serialised data contains a field that /// does not exist on the destination type because the serialised data is from a newer version of the type and the destination is an older version of that type that /// has never had the field - in that case, this method wil return null. /// </summary> public DeprecatedPropertySettingDetails TryToGetPropertySettersAndFieldsToConsiderToHaveBeenSet(Type typeToLookForPropertyOn, string fieldName, string typeNameIfRequired, Type fieldValueTypeIfAvailable) { if (typeToLookForPropertyOn == null) { throw new ArgumentNullException(nameof(typeToLookForPropertyOn)); } if (string.IsNullOrWhiteSpace(fieldName)) { throw new ArgumentException($"Null/blank {nameof(fieldName)} specified"); } var propertySetters = new List <Tuple <PropertyInfo, MemberUpdater> >(); var fieldsToConsiderToHaveBeenSetViaDeprecatedProperties = new List <FieldInfo>(); var propertyName = BackingFieldHelpers.TryToGetNameOfPropertyRelatingToBackingField(fieldName) ?? fieldName; while (typeToLookForPropertyOn != null) { if ((typeNameIfRequired == null) || (typeToLookForPropertyOn.AssemblyQualifiedName == typeNameIfRequired)) { // TODO: Not sure about this fieldValueTypeIfAvailable business! var deprecatedProperty = typeToLookForPropertyOn.GetProperties(BinaryReaderWriterShared.MemberRetrievalBindingFlags) .Where(p => (p.Name == propertyName) && (p.DeclaringType == typeToLookForPropertyOn) && (p.GetIndexParameters().Length == 0) && (((fieldValueTypeIfAvailable == null) && !p.PropertyType.IsValueType) || ((fieldValueTypeIfAvailable != null) && p.PropertyType.IsAssignableFrom(fieldValueTypeIfAvailable))) ) .Select(p => new { Property = p, ReplaceBy = p.GetCustomAttribute <DeprecatedAttribute>()?.ReplacedBy }) .FirstOrDefault(p => p.ReplaceBy != null); // Safe to use FirstOrDefault because there can't be multiple [Deprecated] as AllowMultiple is not set to true on the attribute class if (deprecatedProperty != null) { // Try to find a field that the "ReplacedBy" value relates to (if we find it then we'll consider it to have been set because setting the // deprecated property should set it)) propertySetters.Add(Tuple.Create(deprecatedProperty.Property, GetPropertyWriter(deprecatedProperty.Property))); var field = typeToLookForPropertyOn.GetFields(BinaryReaderWriterShared.MemberRetrievalBindingFlags) .Where(f => (f.Name == deprecatedProperty.ReplaceBy) && (f.DeclaringType == typeToLookForPropertyOn)) .FirstOrDefault(); if (field == null) { // If the "ReplacedBy" value didn't directly match a field then try to find a property that it matches and then see if there is a // backing field for that property that we can set (if we find this then we'll consider to have been set because setting the deprecated // property should set it) var property = typeToLookForPropertyOn.GetProperties(BinaryReaderWriterShared.MemberRetrievalBindingFlags) .Where(p => (p.Name == deprecatedProperty.ReplaceBy) && (p.DeclaringType == typeToLookForPropertyOn) && (p.GetIndexParameters().Length == 0)) .FirstOrDefault(); if (property != null) { var nameOfPotentialBackingFieldForProperty = BackingFieldHelpers.GetBackingFieldName(property.Name); field = typeToLookForPropertyOn.GetFields(BinaryReaderWriterShared.MemberRetrievalBindingFlags) .Where(f => (f.Name == nameOfPotentialBackingFieldForProperty) && (f.DeclaringType == typeToLookForPropertyOn)) .FirstOrDefault(); } } if (field != null) { // Although the field hasn't directly been set, it should have been set indirectly by setting the property value above (unless the [Deprecated] // "ReplaceBy" value was lying) fieldsToConsiderToHaveBeenSetViaDeprecatedProperties.Add(field); } } } typeToLookForPropertyOn = typeToLookForPropertyOn.BaseType; } if (!propertySetters.Any()) { return(null); } Type compatibleCommonPropertyTypeIfKnown = null; foreach (var propertySetter in propertySetters) { if (compatibleCommonPropertyTypeIfKnown == null) { compatibleCommonPropertyTypeIfKnown = propertySetter.Item1.PropertyType; } else if (propertySetter.Item1.PropertyType != compatibleCommonPropertyTypeIfKnown) { if (compatibleCommonPropertyTypeIfKnown.IsAssignableFrom(propertySetter.Item1.PropertyType)) { // If this property is of a more specific type than propertyTypeIfKnown but the current propertyTypeIfKnown could be satisfied by it then record // this type going forward (any other properties will need to either be this type of be assignable to it otherwise we won't be able to deserialise // a single value that can be used to populate all of the related properties - only for cases where there are multiple, obviously) compatibleCommonPropertyTypeIfKnown = propertySetter.Item1.PropertyType; } else if (!propertySetter.Item1.PropertyType.IsAssignableFrom(compatibleCommonPropertyTypeIfKnown)) { // If propertySetter.PropertyType is not the same as propertyTypeIfKnown and if propertyTypeIfKnown is not assignable from propertySetter.PropertyType // and propertySetter.PropertyType is not assigned from propertyTypeIfKnown then we've come to an impossible situation - if there are multiple properties // then there must be a single type that a value may be deserialised as that may be used to set ALL of the properties. Since there isn't, we have to throw. throw new InvalidSerialisationDataFormatException($"Type {typeToLookForPropertyOn.Name} has [Deprecated] properties that are all set by data for field {fieldName} but which have incompatible types"); } } } return(new DeprecatedPropertySettingDetails( compatibleCommonPropertyTypeIfKnown, propertySetters.Select(p => p.Item2).ToArray(), fieldsToConsiderToHaveBeenSetViaDeprecatedProperties.ToArray() )); }