Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)

Entity Framework 4.1 - CodeFirst 개체의 직렬화 시 순환 참조 해결하는 방법 - 두 번째 이야기

지난번 이야기에서 언급했던,

Entity Framework 4.1 - CodeFirst 개체의 직렬화 시 순환 참조 해결하는 방법
; https://www.sysnet.pe.kr/2/0/1086

순환 참조를 해결하는 두 번째 방법을 살펴볼 텐데요. 혹시 이에 대한 힌트가 지난번 오류 메시지에서 약간 포함되어 있었다는 것을 눈치채신 분이 계실까요? ^^

System.Runtime.Serialization.SerializationException: Object graph for type 'System.Collections.Generic.HashSet`1[[SubEntity, App_Code.xdcj8jsd, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]' contains cycles and cannot be serialized if reference tracking is disabled.


결국 "reference tracking" 옵션을 활성화시키면 된다는 이야기인데요. 즉, WCF 서비스의 출력으로 사용되는 DataContractSerializer에 "reference tracking" 옵션을 켜두고 그것을 지정해 주면 되는 것입니다.

이와 관련해서 검색해 보면, 다행히 그 방법이 설명된 글들이 나옵니다.

Preserving Object Reference in WCF
; http://blogs.msdn.com/b/sowmy/archive/2006/03/26/561188.aspx

Specifying Data Transfer in Service Contracts
; https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/specifying-data-transfer-in-service-contracts

순서를 나열해 보면 다음과 같습니다.

  1. svc의 Factory 재정의
  2. ServiceHostFactory에 사용자 정의 ServiceHost 설정
  3. ServiceHost의 OnOpening 메서드를 재정의해서 사용자 정의 DataContractSerializerOperationBehavior 설정
  4. DataContractSerializerOperationBehavior의 CreateSerializer 메서드를 재정의해서 "reference tracking"이 가능한 DataContractSerializer 설정

뭐... 약간은 복잡하긴 하지만, 그래도 할만한 수준입니다. ^^ 지난번 글에서 문제가 되었던 소스 코드로부터 위의 순서를 그대로 적용해 보겠습니다.

우선 Service.svc 파일을 열어서 다음과 같이 ServiceFactory를 지정합니다. (주의: Web Site 모델의 경우, 네임스페이스가 생략되므로 아래와 같이 클래스 명만 지정하면 되지만, Web Application Project의 경우에는 필히 네임스페이스를 같이 지정해 주어야 합니다.)

<% @ServiceHost Language=C# Debug="true" Service="MyService" 
Factory="CustomServiceFactory" CodeBehind="~/App_Code/Service.cs" %>

Factory를 지정했으니, 그 이름에 맞게 클래스 파일을 하나 생성하고 다음과 같이 CreateServiceHost 메서드를 재정의합니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ServiceModel.Activation;

public class CustomServiceFactory : ServiceHostFactory
{
    protected override System.ServiceModel.ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
        return new CustomServiceHost(serviceType, baseAddresses);
    }
}

이어서, CustomServiceHost 클래스 파일을 생성하고 OnOpeneing 메서드를 재정의 해줍니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ServiceModel;

public class CustomServiceHost : ServiceHost
{
    public CustomServiceHost(object singletonInstance, params Uri[] baseAddresses) : base(singletonInstance, baseAddresses) { }

    public CustomServiceHost(Type serviceType, params Uri[] baseAddresses) : base(serviceType, baseAddresses) { }

    protected override void OnOpened()
    {
        base.OnOpened();
    }

    protected override void OnOpening()
    {
        base.OnOpening();
        AddBehavior();
    }
}

AddBehavior에서는 개별 WCF 메서드에 새로운 DataContractSerializerOperationBehavior를 설정해 주면 되는데요. 이에 대해서는 위에서 제가 소개한 "Preserving Object Reference in WCF" 글에 포함된 소스 코드에 따라 다음과 같이 구현해 줄 수 있습니다. (사실, 이 코드에는 문제가 있지만 나중에 지적하겠습니다.)

void AddBehavior()
{
    if (base.Description != null)
    {
        foreach (ServiceEndpoint ep in this.Description.Endpoints)
        {
            foreach (OperationDescription op in ep.Contract.Operations)
            {
                op.Behaviors.Add(new ReferencePreservingDataContractSerializerOperationBehavior(op));
            }
        }
    }
}

거의 다 왔군요. 마지막으로 ReferencePreservingDataContractSerializerOperationBehavior 클래스 파일을 만들고 아래와 같이 CreateSerializer 메서드를 재정의해주면 됩니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ServiceModel.Description;
using System.Runtime.Serialization;
using System.Xml;

public class ReferencePreservingDataContractSerializerOperationBehavior : DataContractSerializerOperationBehavior
{
    public ReferencePreservingDataContractSerializerOperationBehavior(OperationDescription operationDescription)
        : base(operationDescription)
    {
    }

    public override XmlObjectSerializer CreateSerializer(
      Type type, string name, string ns, IList<Type> knownTypes)
    {
        return CreateDataContractSerializer(type, name, ns, knownTypes);
    }

    private static XmlObjectSerializer CreateDataContractSerializer(
      Type type, string name, string ns, IList<Type> knownTypes)
    {
        return CreateDataContractSerializer(type, name, ns, knownTypes);
    }

    public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
    {
        return new DataContractSerializer(type, name, ns, knownTypes,
            0x7FFF /*maxItemsInObjectGraph*/,
            false/*ignoreExtensionDataObject*/,
            true/*preserveObjectReferences*/,
            null/*dataContractSurrogate*/);
    }
}

이걸로 일단 구현은 끝입니다. 빌드하고 실행해 보면, 결과는 어떨까요? ^^

아쉽게도 여전히 오류가 발생합니다. 문제를 추적해 보면, AddBehavior 메서드의 Description.Endpoints 루프가 실행되지 않는 것을 확인할 수 있습니다.

void AddBehavior()
{
    if (base.Description != null)
    {
        foreach (ServiceEndpoint ep in this.Description.Endpoints)
        {
            System.Diagnostics.Debug.WriteLine("실행되지 않음!!!");

            foreach (OperationDescription op in ep.Contract.Operations)
            {
                op.Behaviors.Add(new ReferencePreservingDataContractSerializerOperationBehavior(op));
            }
        }
    }
}

왜냐하면, 이렇게 .svc 파일의 ServiceFactory를 재정의해서 Endpoints를 열람하려면 web.config에서 명시적으로 endpoint 노드를 지정해 주어야만 열람이 되기 때문입니다.

<system.serviceModel>
    <services>
        <service name="MyService">
            <endpoint contract="IMyService" binding="basicHttpBinding"/>
        </service>
    </services>
    
    <behaviors>
        <serviceBehaviors>
            <behavior name="">
                <serviceDebug includeExceptionDetailInFaults="true"/>
                <serviceMetadata httpGetEnabled="true"/>
            </behavior>
        </serviceBehaviors>
    </behaviors>
</system.serviceModel>

다시 테스트를 해볼까요? 하지만, 그래도 오류가 발생할 것입니다. 다행히 오류 메시지를 살펴보면 문제의 원인을 쉽게 파악할 수 있습니다.

An ExceptionDetail, likely created by IncludeExceptionDetailInFaults=true, whose value is:
System.InvalidOperationException: An exception was thrown in a call to a WSDL export extension: ReferencePreservingDataContractSerializerOperationBehavior
 contract: http://tempuri.org/:IMyService ----> System.ArgumentException: Calling IWsdlExportExtension.ExportContract twice with the same ContractDescription is not supported.
   at System.ServiceModel.Description.MessageContractExporter.CreateMessage(MessageDescription message, Int32 messageIndex, Message& wsdlMessage)
   at System.ServiceModel.Description.MessageContractExporter.ExportMessage(Int32 messageIndex, Object state)
   at System.ServiceModel.Description.MessageContractExporter.ExportMessageContract()
   at System.ServiceModel.Description.WsdlExporter.CallExtension(WsdlContractConversionContext contractContext, IWsdlExportExtension extension)
   --- End of inner ExceptionDetail stack trace ---

말 그대로, DataContractSerializerOperationBehavior가 2개 이상 설정되었다는 것! 왜냐하면 기본적으로 모든 WCF 메서드에 DataContractSerializerOperationBehavior가 존재하기 때문인데, 이로 인해 사실상 AddBehavior 코드가 다음과 같이 변경되어야만 합니다.

void AddBehavior()
{
    if (base.Description != null)
    {
        foreach (ServiceEndpoint ep in this.Description.Endpoints)
        {
            foreach (OperationDescription op in ep.Contract.Operations)
            {
                DataContractSerializerOperationBehavior dataContractBehavior =
                    op.Behaviors.Find<DataContractSerializerOperationBehavior>() as DataContractSerializerOperationBehavior;

                if (dataContractBehavior != null)
                {
                    op.Behaviors.Remove(dataContractBehavior);
                }

                op.Behaviors.Add(new ReferencePreservingDataContractSerializerOperationBehavior(op));
            }
        }
    }
}

이제 정말 끝난 걸까요? ^^ 넵. 이것으로 모든 오류 수정이 끝났고, 이제 GetMyEntities WCF 메서드를 호출해도 정상적으로 동작을 합니다.

첨부된 파일은 위의 코드를 포함한 예제 프로젝트입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/26/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 106  [107]  108  109  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11249정성태7/12/201718550오류 유형: 410. LoadLibrary("[...].dll") failed - The specified procedure could not be found.
11248정성태7/12/201725038오류 유형: 409. pip install pefile - 'cp949' codec can't decode byte 0xe2 in position 208687: illegal multibyte sequence
11247정성태7/12/201719382오류 유형: 408. SqlConnection 객체 생성 시 무한 대기 문제파일 다운로드1
11246정성태7/11/201718149VS.NET IDE: 118. Visual Studio - 다중 폴더에 포함된 파일들에 대한 "Copy to Output Directory"를 한 번에 설정하는 방법
11245정성태7/10/201723768개발 환경 구성: 321. Visual Studio Emulator for Android 소개 [2]
11244정성태7/10/201723324오류 유형: 407. Visual Studio에서 ASP.NET Core 실행할 때 dotnet.exe 프로세스의 -532462766 오류 발생 [1]
11243정성태7/10/201720003.NET Framework: 666. dotnet.exe - 윈도우 운영체제에서의 .NET Core 버전 찾기 규칙
11242정성태7/8/201720276제니퍼 .NET: 27. 제니퍼 닷넷 적용 사례 (7) - 노후된 스토리지 장비로 인한 웹 서비스 Hang (멈춤) 현상
11241정성태7/8/201719024오류 유형: 406. Xamarin 빌드 에러 XA5209, APT0000
11240정성태7/7/201721957.NET Framework: 665. ClickOnce를 웹 브라우저를 이용하지 않고 쿼리 문자열을 전달하면서 실행하는 방법 [3]파일 다운로드1
11239정성태7/6/201723431.NET Framework: 664. Protocol Handler - 웹 브라우저에서 데스크톱 응용 프로그램을 실행하는 방법 [5]파일 다운로드1
11238정성태7/6/201720968오류 유형: 405. NT 서비스 시작 시 "Error 1067: The process terminated unexpectedly." 오류 발생 [2]
11237정성태7/5/201722603.NET Framework: 663. C# - PDB 파일 경로를 PE 파일로부터 얻는 방법파일 다운로드1
11236정성태7/4/201725858.NET Framework: 662. C# - VHD/VHDX 가상 디스크를 마운트하지 않고 파일을 복사하는 방법파일 다운로드1
11235정성태6/29/201719974Math: 20. Matlab/Octave로 Gram-Schmidt 정규 직교 집합 구하는 방법
11234정성태6/29/201717316오류 유형: 404. SharePoint 2013 설치 과정에서 "The username is invalid The account must be a valid domain account" 오류 발생
11233정성태6/28/201717247오류 유형: 403. SharePoint Server 2013을 Windows Server 2016에 설치할 때 .NET 4.5 설치 오류 발생
11232정성태6/28/201718232Windows: 144. Windows Server 2016에 Windows Identity Extensions을 설치하는 방법
11231정성태6/28/201718847디버깅 기술: 86. windbg의 mscordacwks DLL 로드 문제 - 세 번째 이야기 [1]
11230정성태6/28/201718022제니퍼 .NET: 26. 제니퍼 닷넷 적용 사례 (6) - 잦은 Recycle 문제
11229정성태6/27/201719261오류 유형: 402. Windows Server Backup 관리 콘솔이 없어진 경우
11228정성태6/26/201716720개발 환경 구성: 320. Visual Basic .NET 프로젝트에서 내장 Manifest 자원을 EXE 파일로부터 제거하는 방법파일 다운로드1
11227정성태6/19/201724482개발 환경 구성: 319. windbg에서 python 스크립트 실행하는 방법 - pykd [6]
11226정성태6/19/201716328오류 유형: 401. Microsoft Edge를 실행했는데 입력 반응이 없는 경우
11225정성태6/19/201715651오류 유형: 400. Outlook - The required file ExSec32.dll cannot be found in your path. Install Microsoft Outlook again.
11224정성태6/13/201718141.NET Framework: 661. Json.NET의 DeserializeObject 수행 시 속성 이름을 동적으로 바꾸는 방법파일 다운로드1
... 106  [107]  108  109  110  111  112  113  114  115  116  117  118  119  120  ...