.NET 4에서의 ISerializable 인터페이스에 대한 주의 사항

오늘은 .NET Framework 4 에서 가장 많은 충돌을 야기할 수 있는 변동 사항 중 하나를 오늘 아티클에서 잠시 다루어보기로 하겠습니다. 바로 ISerializable 인터페이스에 관한 것인데요, Serializable Attribute와 NonSerialized Attribute를 이용하여 제어하는 것 만으로는 불충분한 경우 즐겨 사용해오던 인터페이스입니다. 그렇지만 여기에 급격한 변화 (Breaking Changes)가 .NET Framework 4에 더해지게 되었는데, Partial Trust Mode로 실행되는 어셈블리에서 이 인터페이스를 구현하는 로직을 호출하게 되는 경우, ISerializable 인터페이스의 GetObjectData에 추가된 SecurityCritical Attribute에 의해 호출이 거부됩니다.


저 개인적으로는 XML-RPC의 .NET Framework 버전의 Implementation 라이브러리 (http://www.xml-rpc.net/)를 .NET Framework 4로 업그레이드하여 Windows Azure에 Deploy하면서 이러한 현상을 겪었는데, 이 라이브러리에서 제공하는 Custom Exception 클래스 상의 GetObjectData 메서드가 문제의 원인이 되었습니다. .NET Framework 4 환경에서 사용하도록 별도의 프로젝트 파일을 만들어서 Predefine Condition을 부여하여 ISerializable 인터페이스를 이용하지 않도록 코드를 수정한 이후에는 문제가 잘 해결되었습니다.


만약 ISerializable 인터페이스의 기능을 그대로 가져갈 필요가 있다면, http://msdn.microsoft.com/en-us/library/system.runtime.serialization.isafeserializationdata.aspx 의 내용을 참고하시어 ISafeSerializationData 인터페이스를 구현하는 별도의 코드를 작성해야 합니다. 아래는 MSDN Library에서 발췌한 샘플 코드입니다.



using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
using System.Security;


// [assembly: SecurityCritical(SecurityCriticalScope.Everything)]
// Using the SecurityCriticalAttribute prohibits usage of the
// ISafeSerializationData interface.
[assembly: AllowPartiallyTrustedCallers]
namespace ISafeSerializationDataExample
{
    class Test
    {
        public static void Main()
        {
            try
            {
                // This code forces a division by 0 and catches the
                // resulting exception.
                try
                {
                    int zero = 0;
                    int ecks = 1 / zero;
                }
                catch (Exception ex)
                {
                    // Create a new exception to throw.
                    NewException newExcept = new NewException(“Divided by”, 0);


                    // This FileStream is used for the serialization.
                    FileStream fs =
                        new FileStream(“NewException.dat”,
                            FileMode.Create);


                    try
                    {
                        // Serialize the exception.
                        BinaryFormatter formatter = new BinaryFormatter();
                        formatter.Serialize(fs, newExcept);


                        // Rewind the stream and deserialize the exception.
                        fs.Position = 0;
                        NewException deserExcept =
                            (NewException)formatter.Deserialize(fs);
                        Console.WriteLine(
                        “Forced a division by 0, caught the resulting exception, n” +
                        “and created a derived exception with custom data. n” +
                        “Serialized the exception and deserialized it:n”);
                        Console.WriteLine(“StringData: {0}”, deserExcept.StringData);
                        Console.WriteLine(“intData:   {0}”, deserExcept.IntData);
                    }
                    catch (SerializationException se)
                    {
                        Console.WriteLine(“Failed to serialize: {0}”,
                            se.ToString());
                    }
                    finally
                    {
                        fs.Close();
                        Console.ReadLine();
                    }
                }
            }
            catch (NewException ex)
            {
                Console.WriteLine(“StringData: {0}”, ex.StringData);
                Console.WriteLine(“IntData:   {0}”, ex.IntData);
            }
        }
    }


    [Serializable]
    public class NewException : Exception
    {
        // Because we don’t want the exception state to be serialized normally,
        // we take care of that in the constructor.
        [NonSerialized]
        private NewExceptionState m_state = new NewExceptionState();


        public NewException(string stringData, int intData)
        {
            // Instance data is stored directly in the exception state object.
            m_state.StringData = stringData;
            m_state.IntData = intData;


            // In response to SerializeObjectState, we need to provide
            // any state to serialize with the exception.  In this
            // case, since our state is already stored in an
            // ISafeSerializationData implementation, we can
            // just provide that.


            SerializeObjectState += delegate(object exception,
                SafeSerializationEventArgs eventArgs)
            {
                eventArgs.AddSerializedState(m_state);
            };
            // An alternate implementation would be to store the state
            // as local member variables, and in response to this
            // method create a new instance of an ISafeSerializationData
            // object and populate it with the local state here before
            // passing it through to AddSerializedState.       


        }
        // There is no need to supply a deserialization constructor
        // (with SerializationInfo and StreamingContext parameters),
        // and no need to supply a GetObjectData implementation.



        // Data access is through the state object (m_State).
        public string StringData
        {
            get { return m_state.StringData; }
        }


        public int IntData
        {
            get { return m_state.IntData; }
        }


        // Implement the ISafeSerializationData interface
        // to contain custom  exception data in a partially trusted
       // assembly. Use this interface to replace the
       // Exception.GetObjectData method,
        // which is now marked with the SecurityCriticalAttribute.
        [Serializable]
        private struct NewExceptionState : ISafeSerializationData
        {
            private string m_stringData;
            private int m_intData;


            public string StringData
            {
                get { return m_stringData; }
                set { m_stringData = value; }
            }


            public int IntData
            {
                get { return m_intData; }
                set { m_intData = value; }
            }


            // This method is called when deserialization of the
            // exception is complete.
            void ISafeSerializationData.CompleteDeserialization
                (object obj)
            {
                // Since the exception simply contains an instance of
                // the exception state object, we can repopulate it
                // here by just setting its instance field to be equal
                // to this deserialized state instance.
                NewException exception = obj as NewException;
                exception.m_state = this;
            }
        }
    }
}


또는, 코드의 수정을 최소화하기 위하여 설정 파일 (app.config이나 web.config)에 LegacyCASMode Property를 사용하도록 설정을 구성할 수 있습니다. 이에 대한 내용은 http://msdn.microsoft.com/ko-kr/library/tkscy493.aspx 의 내용을 참고하시면 됩니다. 아래는 MSDN Library에서 발췌한 사용 예시입니다.


<location allowOverride=”false”>
  <system.web>
    <securityPolicy>
      <trustLevel name=”Full” policyFile=”internal” />
      <trustLevel name=”High” policyFile=”web_hightrust.config” />
      <trustLevel name=”Medium” policyFile=”web_mediumtrust.config” />
      <trustLevel name=”Low”  policyFile=”web_lowtrust.config” />
      <trustLevel name=”Minimal” policyFile=”web_minimaltrust.config”/>
    </securityPolicy>
  </system.web>
</location>


<location allowOverride=”false”>
  <system.web>
    <trust level=”Medium” originUrl=”” />
  </system.web>
</location>


<location allowOverride=”true” path=”Default Web Site/Temp”>
  <system.web>
    <trust level=”Medium” originUrl=”” />
  </system.web>
</location>


 

댓글 남기기