What is the correct way to make a custom .NET Exception serializable? -
more specifically, when exception contains custom objects may or may not serializable.
take example:
public class myexception : exception { private readonly string resourcename; private readonly ilist<string> validationerrors; public myexception(string resourcename, ilist<string> validationerrors) { this.resourcename = resourcename; this.validationerrors = validationerrors; } public string resourcename { { return this.resourcename; } } public ilist<string> validationerrors { { return this.validationerrors; } } }
if exception serialized , de-serialized, 2 custom properties (resourcename
, validationerrors
) not preserved. properties return null
.
is there common code pattern implementing serialization custom exception?
base implementation, without custom properties
serializableexceptionwithoutcustomproperties.cs:
namespace serializableexceptions { using system; using system.runtime.serialization; [serializable] // important: attribute not inherited exception, , must specified // otherwise serialization fail serializationexception stating // "type x in assembly y not marked serializable." public class serializableexceptionwithoutcustomproperties : exception { public serializableexceptionwithoutcustomproperties() { } public serializableexceptionwithoutcustomproperties(string message) : base(message) { } public serializableexceptionwithoutcustomproperties(string message, exception innerexception) : base(message, innerexception) { } // without constructor, deserialization fail protected serializableexceptionwithoutcustomproperties(serializationinfo info, streamingcontext context) : base(info, context) { } } }
full implementation, custom properties
complete implementation of custom serializable exception (myserializableexception
), , derived sealed
exception (myderivedserializableexception
).
the main points implementation summarized here:
- you must decorate each derived class
[serializable]
attribute — attribute not inherited base class, , if not specified, serialization failserializationexception
stating "type x in assembly y not marked serializable." - you must implement custom serialization.
[serializable]
attribute alone not enough —exception
implementsiserializable
means derived classes must implement custom serialization. involves 2 steps:- provide serialization constructor. constructor should
private
if classsealed
, otherwise shouldprotected
allow access derived classes. - override getobjectdata() , make sure call through
base.getobjectdata(info, context)
@ end, in order let base class save own state.
- provide serialization constructor. constructor should
serializableexceptionwithcustomproperties.cs:
namespace serializableexceptions { using system; using system.collections.generic; using system.runtime.serialization; using system.security.permissions; [serializable] // important: attribute not inherited exception, , must specified // otherwise serialization fail serializationexception stating // "type x in assembly y not marked serializable." public class serializableexceptionwithcustomproperties : exception { private readonly string resourcename; private readonly ilist<string> validationerrors; public serializableexceptionwithcustomproperties() { } public serializableexceptionwithcustomproperties(string message) : base(message) { } public serializableexceptionwithcustomproperties(string message, exception innerexception) : base(message, innerexception) { } public serializableexceptionwithcustomproperties(string message, string resourcename, ilist<string> validationerrors) : base(message) { this.resourcename = resourcename; this.validationerrors = validationerrors; } public serializableexceptionwithcustomproperties(string message, string resourcename, ilist<string> validationerrors, exception innerexception) : base(message, innerexception) { this.resourcename = resourcename; this.validationerrors = validationerrors; } [securitypermissionattribute(securityaction.demand, serializationformatter = true)] // constructor should protected unsealed classes, private sealed classes. // (the serializer invokes constructor through reflection, can private) protected serializableexceptionwithcustomproperties(serializationinfo info, streamingcontext context) : base(info, context) { this.resourcename = info.getstring("resourcename"); this.validationerrors = (ilist<string>)info.getvalue("validationerrors", typeof(ilist<string>)); } public string resourcename { { return this.resourcename; } } public ilist<string> validationerrors { { return this.validationerrors; } } [securitypermissionattribute(securityaction.demand, serializationformatter = true)] public override void getobjectdata(serializationinfo info, streamingcontext context) { if (info == null) { throw new argumentnullexception("info"); } info.addvalue("resourcename", this.resourcename); // note: if "list<t>" isn't serializable may need work out // method of adding list, show... info.addvalue("validationerrors", this.validationerrors, typeof(ilist<string>)); // must call through base class let save own state base.getobjectdata(info, context); } } }
derivedserializableexceptionwithadditionalcustomproperties.cs:
namespace serializableexceptions { using system; using system.collections.generic; using system.runtime.serialization; using system.security.permissions; [serializable] public sealed class derivedserializableexceptionwithadditionalcustomproperty : serializableexceptionwithcustomproperties { private readonly string username; public derivedserializableexceptionwithadditionalcustomproperty() { } public derivedserializableexceptionwithadditionalcustomproperty(string message) : base(message) { } public derivedserializableexceptionwithadditionalcustomproperty(string message, exception innerexception) : base(message, innerexception) { } public derivedserializableexceptionwithadditionalcustomproperty(string message, string username, string resourcename, ilist<string> validationerrors) : base(message, resourcename, validationerrors) { this.username = username; } public derivedserializableexceptionwithadditionalcustomproperty(string message, string username, string resourcename, ilist<string> validationerrors, exception innerexception) : base(message, resourcename, validationerrors, innerexception) { this.username = username; } [securitypermissionattribute(securityaction.demand, serializationformatter = true)] // serialization constructor private, class sealed private derivedserializableexceptionwithadditionalcustomproperty(serializationinfo info, streamingcontext context) : base(info, context) { this.username = info.getstring("username"); } public string username { { return this.username; } } public override void getobjectdata(serializationinfo info, streamingcontext context) { if (info == null) { throw new argumentnullexception("info"); } info.addvalue("username", this.username); base.getobjectdata(info, context); } } }
unit tests
mstest unit tests 3 exception types defined above.
unittests.cs:
namespace serializableexceptions { using system; using system.collections.generic; using system.io; using system.runtime.serialization.formatters.binary; using microsoft.visualstudio.testtools.unittesting; [testclass] public class unittests { private const string message = "the widget has unavoidably blooped out."; private const string resourcename = "resource-a"; private const string validationerror1 = "you forgot set whizz bang flag."; private const string validationerror2 = "wally cannot operate in 0 gravity."; private readonly list<string> validationerrors = new list<string>(); private const string username = "barry"; public unittests() { validationerrors.add(validationerror1); validationerrors.add(validationerror2); } [testmethod] public void testserializableexceptionwithoutcustomproperties() { exception ex = new serializableexceptionwithoutcustomproperties( "message", new exception("inner exception.")); // save full tostring() value, including exception message , stack trace. string exceptiontostring = ex.tostring(); // round-trip exception: serialize , de-serialize binaryformatter binaryformatter bf = new binaryformatter(); using (memorystream ms = new memorystream()) { // "save" object state bf.serialize(ms, ex); // re-use same stream de-serialization ms.seek(0, 0); // replace original exception de-serialized 1 ex = (serializableexceptionwithoutcustomproperties)bf.deserialize(ms); } // double-check exception message , stack trace (owned base exception) preserved assert.areequal(exceptiontostring, ex.tostring(), "ex.tostring()"); } [testmethod] public void testserializableexceptionwithcustomproperties() { serializableexceptionwithcustomproperties ex = new serializableexceptionwithcustomproperties(message, resourcename, validationerrors); // sanity check: make sure custom properties set before serialization assert.areequal(message, ex.message, "message"); assert.areequal(resourcename, ex.resourcename, "ex.resourcename"); assert.areequal(2, ex.validationerrors.count, "ex.validationerrors.count"); assert.areequal(validationerror1, ex.validationerrors[0], "ex.validationerrors[0]"); assert.areequal(validationerror2, ex.validationerrors[1], "ex.validationerrors[1]"); // save full tostring() value, including exception message , stack trace. string exceptiontostring = ex.tostring(); // round-trip exception: serialize , de-serialize binaryformatter binaryformatter bf = new binaryformatter(); using (memorystream ms = new memorystream()) { // "save" object state bf.serialize(ms, ex); // re-use same stream de-serialization ms.seek(0, 0); // replace original exception de-serialized 1 ex = (serializableexceptionwithcustomproperties)bf.deserialize(ms); } // make sure custom properties preserved after serialization assert.areequal(message, ex.message, "message"); assert.areequal(resourcename, ex.resourcename, "ex.resourcename"); assert.areequal(2, ex.validationerrors.count, "ex.validationerrors.count"); assert.areequal(validationerror1, ex.validationerrors[0], "ex.validationerrors[0]"); assert.areequal(validationerror2, ex.validationerrors[1], "ex.validationerrors[1]"); // double-check exception message , stack trace (owned base exception) preserved assert.areequal(exceptiontostring, ex.tostring(), "ex.tostring()"); } [testmethod] public void testderivedserializableexceptionwithadditionalcustomproperty() { derivedserializableexceptionwithadditionalcustomproperty ex = new derivedserializableexceptionwithadditionalcustomproperty(message, username, resourcename, validationerrors); // sanity check: make sure custom properties set before serialization assert.areequal(message, ex.message, "message"); assert.areequal(resourcename, ex.resourcename, "ex.resourcename"); assert.areequal(2, ex.validationerrors.count, "ex.validationerrors.count"); assert.areequal(validationerror1, ex.validationerrors[0], "ex.validationerrors[0]"); assert.areequal(validationerror2, ex.validationerrors[1], "ex.validationerrors[1]"); assert.areequal(username, ex.username); // save full tostring() value, including exception message , stack trace. string exceptiontostring = ex.tostring(); // round-trip exception: serialize , de-serialize binaryformatter binaryformatter bf = new binaryformatter(); using (memorystream ms = new memorystream()) { // "save" object state bf.serialize(ms, ex); // re-use same stream de-serialization ms.seek(0, 0); // replace original exception de-serialized 1 ex = (derivedserializableexceptionwithadditionalcustomproperty)bf.deserialize(ms); } // make sure custom properties preserved after serialization assert.areequal(message, ex.message, "message"); assert.areequal(resourcename, ex.resourcename, "ex.resourcename"); assert.areequal(2, ex.validationerrors.count, "ex.validationerrors.count"); assert.areequal(validationerror1, ex.validationerrors[0], "ex.validationerrors[0]"); assert.areequal(validationerror2, ex.validationerrors[1], "ex.validationerrors[1]"); assert.areequal(username, ex.username); // double-check exception message , stack trace (owned base exception) preserved assert.areequal(exceptiontostring, ex.tostring(), "ex.tostring()"); } } }
Comments
Post a Comment