-
Notifications
You must be signed in to change notification settings - Fork 934
Manually serialize system types in preparation for .NET Core. #1425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Manually serialize system types in preparation for .NET Core. #1425
Conversation
Too many changes (some of them are breaking) for a minor version. |
src/NHibernate/Impl/CriteriaImpl.cs
Outdated
{ | ||
get | ||
{ | ||
if (persistentClass != null || persistentClassAssemblyQualifiedName == null) return persistentClass; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be set on deserialization
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be set on deserialization
Maybe with a method like private void SetValuesOnDeserialized(StreamingContext context)
marked with [OnDeserialized]
for avoiding implementing the ISerializable
interface.
Introducing this property would then be revert-able.
I am planning on cleaning up the loading of types in the property getters. I didn't initially have the type serialization helper classes written and now that I do, I will spread those around. @hazzik can you clarify what you mean by breaking changes? I mentioned that the binary serialization The only "break" (bucket 1, public contract) I know of is that I marked some classes This is what it will take to avoid disabling lots of serialization for the .NET Core version. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks to me we have two main subjects here:
- Adjusting serialization for .Net Standard compatibility.
- Cleaning serialization.
Some changes for compatibility require adjustments for being mergeable as a minor version.
Most changes for cleaning are potential breaking changes and should be moved to a PR for 6.0 I think.
There are also some serialization fixes about types declared serializable but having serialized members or ancestor which are not. Are those fixes required for .Net Standard compatibility? If no, better do them within the cleanup PR.
typeName = info.GetString("typeName"); | ||
} | ||
|
||
public override void GetObjectData(SerializationInfo info, StreamingContext context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing attribute [SecurityCritical]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in #1469
src/NHibernate/Cfg/Configuration.cs
Outdated
@@ -80,7 +82,7 @@ public class Configuration : ISerializable | |||
protected internal SettingsFactory settingsFactory; | |||
|
|||
#region ISerializable Members | |||
public Configuration(SerializationInfo info, StreamingContext context) | |||
protected Configuration(SerializationInfo info, StreamingContext context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For avoiding a breaking change, let it public but add a comment like:
// 6.0 TODO: switch to protected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to #1469 as a potentially breaking change.
src/NHibernate/Cfg/Mappings.cs
Outdated
@@ -11,7 +11,6 @@ namespace NHibernate.Cfg | |||
/// A collection of mappings from classes and collections to relational database tables. | |||
/// </summary> | |||
/// <remarks>Represents a single <c><hibernate-mapping></c> element.</remarks> | |||
[Serializable] | |||
public class Mappings |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it removed because of its dialect field which is not serializable and is there since 2008?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to #1469 as a potentially breaking change.
@@ -16,7 +16,6 @@ namespace NHibernate.Context | |||
/// Provides a <see cref="ISessionFactory.GetCurrentSession()">current session</see> | |||
/// for current asynchronous flow. | |||
/// </summary> | |||
[Serializable] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be kept and instead mark the _session
field as non serializable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in #1469
@@ -14,7 +14,10 @@ namespace NHibernate.Dialect | |||
public class BitwiseFunctionOperation : ISQLFunction | |||
{ | |||
private readonly string _functionName; | |||
|
|||
[NonSerialized] | |||
private SqlStringBuilder _sqlBuffer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should instead switch SqlStringBuilder
to Serializable
. It should only take adding to it the attribute and adding it to its inner class AddingSqlStringVisitor
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in #1469
System.Type declaringType = info.GetValue<SerializableSystemType>("declaringType")?.GetType(); | ||
string propertyName = info.GetString("propertyName"); | ||
|
||
this._propertyInfo = declaringType?.GetProperty( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid this
.
/// Returns the type, using reflection if necessary to load. Will throw if unable to load. | ||
/// </summary> | ||
/// <returns>The type that this class was initialized with or initialized after deserialization.</returns> | ||
public new System.Type GetType() => _type ?? (_type = _typeName?.TypeFromAssembly(true)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be moved to deserialization constructor, especially if it is confirmed we do not need the TryGetType
.
/// Returns the type, using reflection if necessary to load. Will return null if unable to load. | ||
/// </summary> | ||
/// <returns>The type that this class was initialized with, the type initialized after deserialization, or null if unable to load.</returns> | ||
public System.Type TryGetType() => _type ?? (_type = _typeName?.TypeFromAssembly(false)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this? It seems unused currently. If needed, it looks better to use a Lazy<Type>
for not having it trying to resolve the type at each call when it fails.
|
||
public string AssemblyQualifiedName => _typeName?.ToString() ?? _type?.AssemblyQualifiedName; | ||
|
||
public void GetObjectData(SerializationInfo info, StreamingContext context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing SecurityCritical
attribute.
|
||
public static void AddValueWithType<T>(this SerializationInfo info, string name, T value) | ||
{ | ||
info.AddValue(name, value, typeof(T)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AddValue(string name, object value)
almost does that. Two differences here:
- The native one will use the actual type of the instance, while the extension method will use the compile time type.
- The native one will default to
object
in case ofnull
, which is supported on deserialization (it has a special handling fornull
).
Looking the AddValueWithType
usages, it does not look to me it needs these differences. In such case I would favor using the native AddValue(string name, object value)
instead and remove this extension.
namespace NHibernate.Util | ||
{ | ||
[Serializable] | ||
internal sealed class SerializableSystemType : ISerializable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be implemented using IObjectReference
instead in conjunction with ISerializable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not find any example about IObjectReference
for something else than a custom singleton and I do not really see how it can be used for a type we do not define (System.Type
in this case).
If we try SerializationInfo.AddValue("someTypeField", someTypeField, typeof(SerializableSystemType))
:
- This violate
AddValue
documentation which states:
This parameter must always be the type of the object itself or of one of its base classes.
- This will style serialize the type fields I think (if it works).
Then on deserialization (if it indeed works for serializing despite what the documentation writes), how do we get information on what was actually serialized? By defining fields matching what a System.Type
serialize? If it is that, it seems quite brittle compared to current implementation.
I guess that in fact I do not understand at all how to use IObjectReference
in this context.
Is there an analyzer or something I should be using to find these?
I think I do see how @hazzik thought |
So in fact I am asking you @hazzik, may you give some pointers to explain how it should be used? |
I had checked the master branch by searching all "void GetObjectData" and checking they have the attribute (this is the case). Only those here were missing it. I do not know of a way to detect them excepted having a serialization/deserialization test for all serializable types. |
[Serializable]
public class TestClass : ISerializable
{
[NonSerialized] private Type _type;
public Type Type
{
get => _type;
set => _type = value;
}
public TestClass() {}
protected TestClass(SerializationInfo info, StreamingContext context)
=> _type = (Type) info.GetValue("Type", typeof(Type));
public void GetObjectData(SerializationInfo info, StreamingContext context)
=> info.AddValue("Type", new TypeObjectReference(_type));
}
[Serializable]
public class TypeObjectReference : IObjectReference, ISerializable
{
private readonly Type _type;
public TypeObjectReference(Type type)
=> _type = type;
protected TypeObjectReference(SerializationInfo info, StreamingContext context)
=> _type = Type.GetType(info.GetString("AssemblyQualifiedName"));
public object GetRealObject(StreamingContext context)
=> _type;
public void GetObjectData(SerializationInfo info, StreamingContext context)
=> info.AddValue("AssemblyQualifiedName", _type.AssemblyQualifiedName);
}
public class TestCase
{
[Test]
public void TestSerializingType()
{
BinaryFormatter bf = new BinaryFormatter();
var stream = new MemoryStream();
bf.Serialize(stream, new TestClass
{
Type = typeof(TestCase)
});
stream.Position = 0;
var deserialize = (TestClass) bf.Deserialize(stream);
Assert.That(deserialize.Type, Is.EqualTo(typeof(TestCase)));
}
} |
@hazzik I did not discount your suggestion and looked at how Like you show, Running the following in LINQPad throws an exception on Deserialize (ArgumentException: Object of type 'System.RuntimeType' cannot be converted to type 'UserQuery+ProxyType'): void Main()
{
var stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
var a1 = new SerializedObject();
formatter.Serialize(stream, a1);
stream.Position = 0;
var a2 = (SerializedObject)formatter.Deserialize(stream);
}
// Define other methods and classes here
[Serializable]
public class SerializedObject
{
private ProxyType someType = new ProxyType();
}
[Serializable]
public class ProxyType : IObjectReference
{
public Object GetRealObject(StreamingContext context)
{
// When deserializing this object, return a reference to
// the Type object instead.
return typeof(Object);
}
} |
Ok, it's weird. And also I did not see that use cases (as the PR is too big for review) |
Thanks @hazzik for the example, I was not finding this case and was not understanding it could be used that way. |
@ngbrown I would replace wrap/unwrap with conversion operators (probably implicit). This will allow making a lot fewer changes.
|
The commits are in order of clean-up first, then .NET Core required changes last, so splitting isn't difficult. My question is; is the serialization stream change also a 6.0 change? Splitting this pull request will probably result in three separate ones, one with a bunch of "6.0 TODO", then a 6.0 one to actually make the change, and then finally a pull request for .NET Core compatible serialization, which I'm guessing is 6.0 also. Serialization isn't strictly required for .NET Core, but lots of tests have to be skipped without it. |
For me this gray area is ok as a minor (5.1) change. (It would not as a patch (5.0.2).)
Not useful in my opinion if a PR for actually doing those changes is already here. |
529127e
to
fd17962
Compare
I'm about half-way through re-working this pull request. I'm now using The |
48c30c5
to
c44f963
Compare
c44f963
to
8cf1614
Compare
I've finished the re-working of this pull request. I will be using this as a basis for the .NET Core port to not disable so many unit tests in that pull request. |
@ngbrown I don't understand why you use |
…ore. Serialize System.Type, ConstructorInfo, FieldInfo, MethodInfo, and PropertyInfo using only basic types.
Serialize System.Type, ConstructorInfo, FieldInfo, MethodInfo, and PropertyInfo using only basic types.
87d5f71
to
c0ac1e4
Compare
This can't help with serializing delegates ( I did rebase to head, but made no other changes. The |
Please do not merge yet |
Can you please elaborate?
This PR looks to me as matching "We might provide these in the future if we want to.". And it would remove that point from your PR, allowing it to be more lightweight. |
First, this PR has a lot of unrelated changes (randomly renamed properties, method changes, etc). That makes the review is almost impossible as it is not clear what is the intended change and what is changed BTW. Second, it’s not clear what level of serialization compatibility is intended. It’s clearly not [backward] compatible with 5.0.
The #1523 has nothing to do with this PR or with the serialization in general. That PR is only about providing a .net core version. In #1523 I have chosen not to provide any serialization means. In regards serialization we have these options:
My preferred option is 2 with probability to implement 3 in the future.
I had no intention to implement the serialization in my PR. |
I have thoroughly reviewed them all, and I have done this multiple times, due to long life time of this PR. Yes it is a bit more time consuming, but I have made the effort each time I have reviewed it.
I think the ability to deserialize in 5.1 something serialized in 5.0 is off target. I am quite sure it is already not guaranteed due to currently merged PRs in 5.1, and it should not be considered for a minor release anyway in my view. This would be limiting for little benefits. I think serialization compatibility should be a target only for release increments (5.0.x series currently). That is not for no reasons this is a grayish area. I still think this PR is a better option for handling the subject than the two other options. It is ready, reviewed many times, already well polished, and offer a better support of the feature for all cases than the two other options. |
Close in lieu of #1652. |
@ngbrown Hi. I do not know if it will give you any value, but on the FluentNHibernate side we have finished using serialization surrogates for our cloner Thanks in advance for the amazing work guys! |
Serialize
System.Type
,ConstructorInfo
,FieldInfo
,MethodInfo
, andPropertyInfo
using only basic types.I used reflection generated list of
[Serializable]
types in .NET Core 2.0 to find and replace fields in serialized types.This partially addresses #889 - NH3853. While not switching away from binary serialization, it should be compatible with .NET Core (to be verified).
If an application is serializing NHibernate internals (like
ISession
or some of the exceptions that are now completely serialized likePropertyNotFoundException
) into permanent storage and expects them to be rehydrated with any future version of NHibernate, then this will break that. Applications should only expect binary serialization compatibility within a very limited version range.