JSON Parsing


 웹 페이지에서 스트림을 통해 얻어온 값을 무턱대고 JSON 문자열을 평가하면 보안 문제가 있을 수 있기 때문에 바람직 하지 않다. 가능하면 JSON.parse()를 사용하는 것이 최선책이다. 이 메소드는 ES5(ECMAScript 5.0)부터 포함되었으나 일부 예전 버전의 브라우저에서는 지원하지 않기 때문에 JSON.org의 라이브러리(http://www.json.org/json2.js)를 이용해서 사용할 수 있다.


[코드1] JSON으로 파싱


 만약 jQuery를 사용하고 있다면 $.parseJSON(jsonString)으로 대신 할 수 있다. 그리고 JSON.parse()의 반대는 JSON.stringify()이다. 이 메소드는 객체 또는 배열을 인자로 받아 문자열로 serialization을 한다. 이 메소드를 통해서 쿠키에 값을 저장 또는 sessionStorage같은 곳에 저장할 수 있다.



[코드2] 확인 코드


isArray


 자바스크립트를 개발하다가 보면 객체를 반별할 때 넘어온 객체가 일반적인 객체인지 배열인지를 판단하여 다르게 수행해야 할 때가 있다. 다음과 같은 코드로 알아내 보도록 해보자.


[코드1] 잘못된 방법으로 Array 인지 판단


 위 코드를 의도한 바와 다르게 판단하고 있다. 그래서 아래와 같이 Array객체에 추가 프로퍼티를 추가해서 확인하는 코드를 작성 하였다.


[코드2] 프로퍼티에 추가하고 배열여부 판단


 위와 같이 웹 페이지에서 추가하여 실행하면 정상적으로 판단을 할 것이다.

Task.ContinueWith



 Task 기반 비 동기 작업을 수행하다가 보면 순차적으로 수행해야 하는 작업이 있을 수 있다. 그럴때마다 await 키워드로 다시 비동기를 실행하지 않고 ContinueWith를 사용하면 순차적인 비 동기를 수행 할 수 있다. 아래 구문을 살펴 보자.


public async void TAP_AsyncAwait_TestMethod2()
{
    //.Net framework 4.5에 추가된 메소드로 
    //Task.Factory.StartNew 래핑한다.
    var returnValue = await Task.Run(() => { /* Something */ return 10; })
        //10을 리턴하면 아래 구문을 수행 한다.
        .ContinueWith(_result => { return _result.Result + 100; })
        //110을 리턴하면 아래 구문을 수행 한다.
        .ContinueWith(_return => { Thread.Sleep(1000); return _return.Result + 200; })
        //1초 후 310을 리턴하면 아래 구문을 수행 한다.
        //결과 값을 String으로 반환 한다.
        .ContinueWith(_return => { return _return.Result.ToString(); });
 
    Console.WriteLine(returnValue);       
}

[코드1] ContinueWith로 비 동기 순차 실행


 이와 같이 사용하여 순차적인 비 동기 실행을 구현하여 await를 남발하지 않도록 하면 될 것이다.



소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       





Task.Run() 메소드


 .Net framework 4.5로 업그레이드 되면서 기존에 사용하고 있던 Task.Factory.StartNew() 메소드가 다음의 구문으로 쉽게 사용할 수 있도록 추가 되었다. 

//Factory를 통해 비 동기 Task를 반환
Task.Factory.StartNew(() => { /* Something */ });
 
//.Net framework 4.5에 추가된 메소드로 
//Task.Factory.StartNew 래핑한다.
Task.Run(() => { /* Something */ });

[코드1] 비 동기 실행 Task 수행 코드


 물론 위 코드도 Generic을 지원하고 있으며 .Net framework 4.5에서는 간결하게 래핑한 Run() 메소드를 사용하는걸 권장한다. 다만 일반 사용 보다는 정교하게 비 동기 작업에 대해서 컨트롤을 해야 할 때는 기존과 같은 방법으로 Factory의 StartNew를 사용해서 사용해야 할 것이다.



소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       




C# Task-based Asynchronous Pattern - TAP(2)

async / await keyword

http://msdn.microsoft.com/ko-kr/library/hh873175.aspx

http://msdn.microsoft.com/en-us/library/hh873175.aspx


http://www.albahari.com/threading/part2.aspx#_Thread_Safety


2013/02/26 - [.Net Framework] - [Async]Task-based Asynchronous Pattern - TAP(1) Async #7


 이번 포스트는 지난 시간에 이어 TAP에 대해서 더 깊이 있게 알아 볼 수 있는 시간을 가져 보도록 하겠습니다. 지난 포스트의 마지막 부분에서 외부 시스템에서 정보를 가져오는( I/O, Network, ...)을 통해 예제를 통해 비 동기로 실행하면 총 수행 시간을 절약할 수 있는 방안에 대해 알아 보았다. 이 번에는 .Net framework 4.5가 나오면서 C#이 5.0으로 업그레이드가 되었다. 그러면서 비 동기 컨트롤을 지원하는 키워드인 async/await가 추가 되어 더욱 편리하고 안전하게 사용할 수 있게 되었다. 이제 새로 추가된 기능으로 비 동기 구현을 해 보도록 하자. 

/// <summary>
/// async/await 키워드를 이용해서 비 동기 실행
/// </summary>
[TestMethod]
public async void TAP_AsyncAwait_TestMethod1()
{
    //비 동기 실행을 하지만 결과값이 리턴이 될 때까지 대기 한다.
    var returnValue = await Task.Factory.StartNew(() => 
        {
            Debug.WriteLine("TAP_AsyncAwait_TestMethod1 비 동기 실행 시작");
            Thread.Sleep(1000);
            Debug.WriteLine("TAP_AsyncAwait_TestMethod1 비 동기 실행 종료");
            return 10;
        });
 
    //정상 수행 여부 체크
    Assert.AreEqual(10returnValue);
}

[코드1] async/await로 비 동기 실행

(테스트 프로젝트에서는 정상적으로 실행 되지 않아 아래 Console에서 확인 한다)


class Program
{
    static void Main(string[] args)
    {
        Program p = new Program();
        p.TAP_AsyncAwait_TestMethod1();
 
        Console.ReadKey();
    }
 
    public async void TAP_AsyncAwait_TestMethod1()
    {
        //비 동기 실행을 하지만 결과값이 리턴이 될 때까지 대기 한다.
        var returnValue = await Task.Factory.StartNew(() =>
        {
            Console.WriteLine("TAP_AsyncAwait_TestMethod1 비 동기 실행 시작");
            Thread.Sleep(1000);
            Console.WriteLine("TAP_AsyncAwait_TestMethod1 비 동기 실행 종료");
            return 10;
        });
 
        //값 확인
        Console.WriteLine(returnValue);
            
    

[코드2] Console에서 async/await로 비 동기 실행


 "코드2"에서와 같이 TAP_AsyncAwait_TestMethod 함수의 리턴 타입 앞에 'async' 키워드를 이용해서 이 함수는 비 동기로 수행 되는 메소드라고 알려줘야 한다 이렇게 해야지만 메소드 내부에서 await를 사용할 수 있다. 그리고 메소드 내부에서 비 동기로 수행되는 구문에서 await를 이용해서 결과값을 받아 오면 된다. 이렇게 간단한 규칙만으로 비 동기 실행을 절차적인 모양(기존 절차지향 언어에서 비 동기를 구현 할 때 Context를 맞춰줘야 하는 복잡한 지식 없이 절차 지향의 구문의 모양처럼 작성 하면서 메소드의 동작 방식은 비 동기로 구현 한다)으로 작성 하도록 도와 준다. 한번 직접 실행하여 분석해 보도록 하자. 생각 보다 쉽게 비 동기 지원 메소드를 작성 하고 사용할 수 있다. 


 이 기능은 프레임워크에서 지원되는 기능이 아닌 컴팔일러 단에서 지원하는 기능이며 내부적으로 TAP(1)에 설명된 사항 처럼 Task 객체를 이용해서 비 동기를 구현 하도록 내부적으로 구현 한다. .Net framework 4.5에서는 이렇게 쉽게 비 동기를 지원하는 편리한 기능을 추가 하였다. 그렇지만 모든 메소드를 비 동기로 작성해야 하는 필요성은 없으며 I/O, Network, DB와 같이 상대적으로 느린 시스템에서 데이터를 컨트롤 하는 부분에서만 비 동기를 사용할 것을 적극 권장하는 바이다.


객체 생성시 new를 강제할 수 있는 패턴



 자바 스크립트 라이브러리를 사용할 때 new 키워드를 사용하지 않고 객체로 선언하였다고 생각하고 사용할 때가 있다.  이런 오류 때문에 정상적으로 동작하지 않고 디버깅에 많은 시간을 할 애 할 수 있다. 그렇지만 new를 사용하고 객체를 생성하지 않더라도 자체적으로 새로운 객체를 생성해서 반환해 주는 패턴이 있다. 우선 아래 코드를 보고 이해를 돕고자 한다.

<script type="text/javascript">
    //객체로 사용할 함수
    function NewInstance1() {
        this.name = 'name';
        this.age = 0;
    }
 
    //new를 사용해서 객체 생성
    var newObject1 = new NewInstance1();
    console.log(newObject1.name);
 
    //new를 사용하지 않고 객체 생성
    var newObject2 = NewInstance1();
    console.log(newObject2.name);   //에러 발생
</script>

[코드1] 객체 생성 및 사용 예제


 위 코드에서는 일반적인 객체를 선언하고 두번째 코드에서 new를 사용해서 객체의 인스턴스를 생성하여 값을 정상적으로 사용할 수 있었다. 하지만 두번째 객체의 인스턴스 생성시 new를 사용하지 않고 사용하면 에러가 발생하게 된다. Javascript 엔진이 new 키워드를 만들면 안에서 사용하는 this의 범위가 달라지게 하는 특별한 수행 덕분에 발생하는 이유다. 이와 같은 패턴으로 선언되어 있으면 개발자의 실수로 인해 전체 웹 페이지에 영향을 미칠 수 있는 잠재적인 오류가 내재화 되어 있다. 그렇다면 어떻게 하면 실수를 미연에 방지 할 수 있을까. 아래와 같은 패턴으로 실수를 방지 할 수 있다.  아래 코드를 보자.

//객체로 사용할 함수
function NewInstance2() {
    if (!(this instanceof NewInstance2)) {
        return new NewInstance2();
    }
 
    this.name = 'name';
    this.age = 0;
}
 
//new를 사용해서 객체 생성
var newObject3 = new NewInstance2();
console.log(newObject3.name);
 
//new를 사용하지 않고 객체 생성
var newObject4 = NewInstance2();
console.log(newObject4.name);   //에러 발생

[코드2] self 인스턴스 생성 패턴



 위와 같은 코드로 만들어진 객체는 개발자의 실수를 방지할 수 있게 할 수 있을 것이다. 그리고 prototype을 통해 추가된 프로퍼티들을 사용할 수 있는 장점이 있다.



소스 코드 자체에 주석과 직관적인 코딩으로 충분히 파악이 가능할 것으로 예상하므로 별도의 설명을 생략하도록 하겠습니다. 포스트의 내용이 장황한 설명 보다는 주석과 소스코드 자체 만으로도 이해할 수 있도록 하기 위해 노력하였습니다.. 실제 개발에서도 필요한 소스는 단순히 Copy & Paste 만으로도 사용할 수 있습니다. 그리고 주석을 이용해 nDoc이나 별도의 자동 Document 제작 유틸로 API 문서를 만드는 데에도 도움이 되었으면 한다. 
※ DOC에 대한 프로그램 정보 Util link

ing™       


Notifications - Push, Pull, Stream Notification #6


 Exchange 2013을 기반으로 개발을 진행하면서 맞닥뜨리게 된 케이스에 대해서 공유 하고자 한다. 이전에 잠깐 EWS(Exchange Web Service)를 통해 간단한 기능을 사용해본 경험이 전부라서 하나의 기능을 구현하기 위해 여러가지 방안에 대해서 바위에 계란치기로 직접 부딪혀 볼 수 밖에 없었다. 이런 힘들고 시간 싸움을 줄이는데 도움을 드리는데 일조 하고자 이곳에 공유 하고자 합니다. 비록 덜 정제 되고 문서 미비할 수 있지만 참고 사항으로 알아 두셨으면 합니다. 



 이제 마지막으로 남은 Pull Notification에 대해서 알아 보도록 하자. 원래 바로 이전 포스트에서 Pull까지 마치려고 하였으나 Listener의 소스가 너무 길어서 한번에 언급하기에는 다소 무리라고 판단이 되었다. 그래서 6회까지 이어지게 되었지만 너그러이 이해해 주시면 좋겠습니다. 


 본격적인 진행에 앞서서 Exchange  2013에서는 Pull Notification 관련 이벤트가 전형 발생하지 않고 있다. Powershell에서 Exchange 설정을 수정해야만 동작하는지는 확인하지 못하였지만 기본 상태에서는 동작하지 않는다. 그렇지만 Exchange 2010을 위해서 코드만이라도 살펴 보도록 하겠다. 


 Pull Notification 방식은 다른 어떤 방법보다 간단한 구조라서 사용 법이 간단하다.

PullNotificationListener pull = null;
 
for (int i = 0i < 1000i++)
{
    foreach (var member in Members.GetUsers())
    {
        try
        {
            string watermask = null;
            watermask = "AQAAAOlS/PGuZXZNiGy4pQo8RMLUKhkAAAAAAAA=";
            //watermask를 포함한 이후의 이벤트를 가져온다.
            pull = new PullNotificationListener(memberwatermask);
            Console.WriteLine(member);
            pull.Pulling();
            Thread.Sleep(10000);
        }
        catch (Exception ex) { Console.WriteLine(member + " Error " + ex.Message); }
    }
}

[코드1] Pull Notification 객체로 요청한다.


 위 코드는 PullNotificationListener을 실행 하도록 요청하는 클래스이며 "코드2"에서 실질적으로 Pulling을 진행 한다. 

/// <summary>
/// Pull Notification 이벤트 처리 클래스
/// </summary>
public class PullNotificationListener
{
    /// <summary>
    /// 사용자 email 주소
    /// </summary>
    private string address = null;
    /// <summary>
    /// Watermask 값
    /// </summary>
    private string watermark = null;
 
    /// <summary>
    /// 생성자
    /// </summary>
    /// <param name="address"></param>
    /// <param name="watermark"></param>
    public PullNotificationListener(string addressstring watermark)
    {
        this.address = address;
        this.watermark = watermark;
    }
 
    /// <summary>
    /// Pull notification을 실행한다.
    /// </summary>
    public void Pulling()
    {
        var service = EWSHelper.GetService(address);
 
        //timeOut를 10분으로 세팅 하였다.
        var pullSubscription = service.SubscribeToPullNotifications(EWSHelper.GetNotificationFolderId(), 10watermarkEWSHelper.GetNotificationEventTypes());
 
        //비 동기 실행
        //IAsyncResult result = pullSubscription.BeginGetEvents(new AsyncCallback(Subscription), pullSubscription);
        //비 동기 실행이 완료 할 때까지 대기 하도록 한다.
        //WaitHandle.WaitAll(new[] { result.AsyncWaitHandle });
 
 
        //동기 실행
        var events = pullSubscription.GetEvents();
 
        Console.WriteLine("start");
        foreach (var itemEvent in events.ItemEvents)
        {
            Console.WriteLine("{0}, {1}, {2}, watermark {3}"addressitemEvent.ItemIdDateTime.Now.ToString());
        }
        Console.WriteLine("end");
    }
 
    public void Subscription(IAsyncResult result)
    {
        Console.WriteLine("Subscription Start");
 
        //PullSubscription 변환
        var pullSubscription = result.AsyncState as PullSubscription;
 
        var events = pullSubscription.EndGetEvents(result);
            
        foreach (var itemEvent in events.ItemEvents)
        {
            Console.WriteLine("{0}, {1}, {2}, watermark {3}"addressitemEvent.ItemIdDateTime.Now.ToString());
        }
 
        //구독을 중단한다.
        //pullSubscription.Unsubscribe();
 
        Console.WriteLine("Subscription End");
    }
}

[코드2] Pulling 클래스


 코드에 대한 부가 설명을 하자면 "코드2"에서 동기 방식과 비 동기 방식으로 수행하는 코드가 작성되어 있지만 비 동기 방식은 주석처리가 되어 있고 동기 방식으로 수행 되도록 작성되었다. Exchange 2013에서 두가지 방식으로 테스트를 진행 하였으나 둘다 동작하지는 않았다. 어느 곳의 공개된 소스를 찾아 봐도 위와 같은 흐름으로 되어 있으며 더 이상 진행하지를 못하였다. ( 비 동기 실행의 코드 주석을 풀면 비 동기로 실행이 되고 있다 )


결론적으로 Exchange 2013에서는 Pull, Stream notification 방식으로 이벤트 정보를 받아서 치리해야 한다.

Notifications - Push, Pull, Stream Notification #5


 Exchange 2013을 기반으로 개발을 진행하면서 맞닥뜨리게 된 케이스에 대해서 공유 하고자 한다. 이전에 잠깐 EWS(Exchange Web Service)를 통해 간단한 기능을 사용해본 경험이 전부라서 하나의 기능을 구현하기 위해 여러가지 방안에 대해서 바위에 계란치기로 직접 부딪혀 볼 수 밖에 없었다. 이런 힘들고 시간 싸움을 줄이는데 도움을 드리는데 일조 하고자 이곳에 공유 하고자 합니다. 비록 덜 정제 되고 문서 미비할 수 있지만 참고 사항으로 알아 두셨으면 합니다. 



 이번에는 Listener에 대해서 살펴 보자. 이 클래스는 MBX에서 보내준 Push 정보인 XML을 HttpListener을 통해 받아 ListenEventArgs로 변환해 주고 연결된 이벤트(EventReceived)를 통해 핸들러로 넘겨 주는 작업을 한다. 이제 아래 코드에서 Listener의 전체 코드를 살펴 보자

/// <summary>
/// Push Notification을 보내 주면 HttpListener을 통해 받은 데이터를 파싱하고 연결된 이벤트를 발생한다.
/// </summary>
public class PushConsoleListener
{
    #region Fidles
    /// <summary>
    /// HttpListener을 통해서 MBX에서 보내준 정보를 받는 역할을 한다.
    /// </summary>
    private HttpListener _Listener = null;
    private string _ListenURi = "";
    private int _ListenPort = int.Parse(ConfigurationManager.AppSettings["PullNotificationListenPort"].ToString()); //36728
    private List<string> _Requests;
    /// <summary>
    /// MBX에서 정보를 보내주고 넘겨줄 리턴 값
    /// </summary>
    private string _NotificationResponse = "";
    //내부 로그 작업에 필요한 객체
    private ClassLogger _Logger = null;
    private string _subscriptionId = "";
    #endregion
 
    //이벤트 델리게이트 선언
    public delegate void ListenEventHandler(object senderListenEventArgs a);
    //이벤트 핸들로 선언
    //별도의 핸들러에서 MBX에서 보내준 정보를 가지고 작업을 할 수 있도록 해준다.
    //다른 클래스에서는 XML파싱이나 HttpListenr에 대해서 전혀 고려할 필요 없어 이 이벤트 처리에 대해서만 신경 쓰면 된다.
    public event ListenEventHandler EventReceived;
 
    //노드를 찾아가기 쉽도록 하기 위해 선엄 됨
    const string m = "http://schemas.microsoft.com/exchange/services/2006/messages";
    const string t = "http://schemas.microsoft.com/exchange/services/2006/types";
 
    public PushConsoleListener(ClassLogger Logger)
    {
        #region 하드코딩(이해하기 쉽도록 수정 된 코드)
        _NotificationResponse = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
        _NotificationResponse += "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">";
        _NotificationResponse += "  <soap:Body>";
        _NotificationResponse += "    <m:SendNotificationResult>";
        _NotificationResponse += "      <m:SubscriptionStatus>OK</m:SubscriptionStatus>";
        _NotificationResponse += "    </m:SendNotificationResult>";
        _NotificationResponse += "  </soap:Body>";
        _NotificationResponse += "</soap:Envelope>";
        #endregion
 
        #region 리소스 파일에서 읽어 옴
        //리소스에 포함된 XML 파일 정보륵 가져온다.
        //이 작업은 MBX에서 HttpListener로 보내주고 정상적으로 Listen을 했는지 여부를 보내주기 위한 정보를 가져온다.
        //예제를 들기 위한 코드로서 에러가 발생 한다면 하드코딩된 데이터를 가지고 작업하면 됩니다.
        StreamReader oReader = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("NoneProcess.XML.EWSSendNotificationResult.xml"));
        _NotificationResponse = oReader.ReadToEnd();
        oReader.Close();
        #endregion
 
        _Requests = new List<string>();
        _Listener = new HttpListener();
 
        _Logger = Logger;
 
        StartListening();
    }
 
    ~PushConsoleListener()
    {
        Close();
    }
 
    /// <summary>
    /// 핸들러가 연결되어 있으면 이벤트를 발생 시켜서 정보를 넘겨 준다.
    /// </summary>
    /// <param name="e"></param>
    protected virtual void OnNotificationReceived(ListenEventArgs e)
    {
        ListenEventHandler handler = EventReceived;
 
        if (handler != null)
        {
            //이벤트고 연결되어 있으면 발생 시킴.
            handler(thise);
        }
    }
 
    private void StartListening()
    {
        //환결 설정 파일에서 HttpListener이 어떤 서버에서 구동되는지 가져 온다.
        //예로 테스트나 구동되는 Host의 IP를 넣어 주면 된다.
        //HttpListener은 관리자 권한으로 실행 해야 정상적인 동작을 할 수 있다.
        string sMachineName = ConfigurationManager.AppSettings["PullNotificationListenerUrlPrefix"].ToString();   //127.0.0.1 Environment.MachineName;
        //try
        //{
        //    System.DirectoryServices.ActiveDirectory.Domain oDomain = System.DirectoryServices.ActiveDirectory.Domain.GetComputerDomain();
        //    if (!String.IsNullOrEmpty(oDomain.Name))
        //        sMachineName = sMachineName + "." + oDomain.Name;
        //}
        //catch { }
 
 
        //_ListenURi = "http://" + sMachineName + ":" + _ListenPort + "/" + AppDomain.CurrentDomain.ToString() + "/";
        _ListenURi = "http://" + sMachineName + ":" + _ListenPort + "/" + "NoneProcessEWSTest" + "/";
        _Listener.Prefixes.Clear();
        //HttpLisener에 Prefix를 세팅해서 응답 받을 수 있도록 세팅 함.
        _Listener.Prefixes.Add(_ListenURi);
 
        //호스팅 정보를 콘솔에 알려 준다.
        Console.WriteLine(_ListenURi);
 
        try
        {
            //HttpListenr을 시작한다.
            //위에서 세팅된 ListenURi 주소로 호스팅을 시작한다.
            _Listener.Start();
            //세팅된 Prefix로 접속하면 GetContext에서 AsyncCallback가 실행 되도록 한다.
            _Listener.BeginGetContext(new AsyncCallback(ListenerCallback), _Listener);
        }
        catch (Exception ex)
        {
            throw;
        }
    }
 
    /// <summary>
    /// 실행 될 포트
    /// </summary>
    public int Port
    {
        get
        {
            return _ListenPort;
        }
        set
        {
            if (value == _ListenPortreturn;
            _Listener.Stop();
            _ListenPort = value;
            StartListening();
        }
    }
 
    /// <summary>
    /// 실행 될 URI 주소
    /// </summary>
    public string URi
    {
        get
        {
            return _ListenURi;
        }
    }
 
    /// <summary>
    /// Subscription Id
    /// </summary>
    public string SubscriptionId
    {
        get
        {
            return _subscriptionId;
        }
        set
        {
            _subscriptionId = value;
        }
    }
 
    /// <summary>
    /// MBX에서 보내준 정보를 받을 때 마다 Callback가 실행 된다.
    /// </summary>
    /// <param name="result"></param>
    public void ListenerCallback(IAsyncResult result)
    {
        //SubscribeToPushNotifications 등록 시 설정 된 frequency 설정 값대로 특정 주기로 서비스가 살아 있는지 확인 한다.
        //정상적으로 동작하지 않으면 MBX에서는 Push를 중단한다.
        //Console.WriteLine("Exchange서버에서 listener가 살아 있는지 점검 : {0}", DateTime.Now.ToString());
        try
        {
            HttpListener listener = (HttpListener)result.AsyncState;
 
            System.Threading.Tasks.Task.Factory.StartNew(() =>
            {
                //결과값을 받아 오기 위한 처리
                HttpListenerContext context = listener.EndGetContext(result);
                //HttpListenerContext에서 Request객체를 할당
                HttpListenerRequest request = context.Request;
 
                //Console.WriteLine("Local end point: {0}", request.LocalEndPoint.ToString());
                //Console.WriteLine("Remote end point: {0}", request.RemoteEndPoint.ToString());
                                        
                string sRequest = "";
 
                using (StreamReader reader = new StreamReader(request.InputStream))
                {
                    //MBX에서 보내준 XML 파일을 읽어 온다.
                    sRequest = reader.ReadToEnd();
                }
 
                //파싱 및 이벤트 전달 하도록 한다.
                DetermineResponse(sRequestcontext.Responserequest);
            });
 
            _Listener.BeginGetContext(new AsyncCallback(ListenerCallback), _Listener);
        }
        catch (Exception ex) { Console.WriteLine(ex.Message); }
    }
 
    /// <summary>
    /// 파싱 및 이벤트 전달 하도록 한다.
    /// </summary>
    /// <param name="Request">XML 전문 정보</param>
    /// <param name="Response">HttpListenerResponse</param>
    /// <param name="request">HttpListenerRequest</param>
    private void DetermineResponse(string RequestHttpListenerResponse ResponseHttpListenerRequest request)
    {
        //정상적인 XML 파일인지 체크
        if (Request.Contains("exchange"&& Request.Contains("SendNotificationResponseMessage"))
        {
            //MBX에 정상적으로 이벤트를 받았다고 리턴 해줌.
            WriteSubscriptionResponse(Response);
            //파싱하고 이벤트 발생시킴
            RaiseEvents(Requestrequest);
        }
 
        Response.OutputStream.Flush();
        //Response의 Stream을 닫는다.
        Response.Close();
    }
 
    /// <summary>
    /// 정상적으로 이벤트를 받았다고 리턴 해준다.
    /// </summary>
    /// <param name="Response">HttpListenerResponse</param>
    private void WriteSubscriptionResponse(HttpListenerResponse Response)
    {
        //정상적이라고 Response에 넘긴다.
        byte[] buffer = System.Text.Encoding.UTF8.GetBytes(_NotificationResponse);
        Response.ContentLength64 = buffer.Length;
        Response.ContentType = "text/xml";
        using (Stream output = Response.OutputStream)
        {
            output.Write(buffer0buffer.Length);
        }
 
        Log(_NotificationResponse"Notification Response");
    }
 
    /// <summary>
    /// XML 파싱해서 HttpListenEventArgs를 생성하고 이벤트를 
    /// </summary>
    /// <param name="Notifications"></param>
    /// <param name="request"></param>
    private void RaiseEvents(string NotificationsHttpListenerRequest request)
    {
        // Process the notifications
        XmlDocument oNotifications = new XmlDocument();
        try
        {
            //XMLDocument로 읽어 들인다. - XML 객체로 인식하도록 한다.
            oNotifications.LoadXml(Notifications);
        }
        catch (Exception ex)
        {
            return;
        }
 
        var responseClassValue = "Error";
        try
        {
            //특정 테그를 검색해서 원하는 노드의 Attribute의 값을 가져온다.
            responseClassValue = oNotifications.GetElementsByTagName("SendNotificationResponseMessage"m)[0].Attributes["ResponseClass"].Value;
        }
        catch { }
 
        //성고 여부 체크
        if (responseClassValue == "Success")
        {
            XmlNodeList nodeListNotifications = oNotifications.GetElementsByTagName("Notification"m);
            if (nodeListNotifications.Count == 0return;
 
            //SubscriptionId를 가져온다.
            XmlNode nodeSubscriptionId = oNotifications.GetElementsByTagName("SubscriptionId"t)[0];
            string subscriptionId = nodeSubscriptionId.InnerText;
 
            // SubscriptionId를 체크해서 정합성 여부를 체크 한다.
            // 이곳에서는 여러 사용자에 대해서 이벤트를 처리 하도록 되어 있기에 정합성 체크를 무시 한다.
            //if (!String.IsNullOrEmpty(_subscriptionId))
            //{
            //    if (subscriptionId != _subscriptionId)
            //        return;
            //}
 
            foreach (XmlNode node in nodeListNotifications[0].ChildNodes)
            {
                //이벤트 종류에 따라 XML 정보가 상이하여 다르게 처리 해야 함.
                switch (node.LocalName)
                {
                    case "StatusEvent":
                        break;
 
                    case "NewMailEvent":
                        //NewMail 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        OnNotificationReceived(new ListenEventArgs(subscriptionId,
                        EventType.NewMail,
                        ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        break;
 
                    case "CreatedEvent":
                        //CreatedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Created,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Created,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "MovedEvent":
                        //MovedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Moved,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Moved,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "CopiedEvent":
                        //CopiedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Copied,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Copied,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "DeletedEvent":
                        //DeletedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Deleted,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Deleted,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "ModifiedEvent":
                        //ModifiedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Modified,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Modified,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
                }
            }
        }
        else
        {
            //subscriptionId를 가져온다.
            XmlNode nodeSubscriptionId = oNotifications.GetElementsByTagName("MessageXml"m)[0];
            string subscriptionId = nodeSubscriptionId.InnerText;
 
            XmlNode nodeMessageText = oNotifications.GetElementsByTagName("MessageText"m)[0];
            string messageText = nodeMessageText.InnerText;
 
            OnNotificationReceived(new ListenEventArgs(subscriptionIdtrue) { Xml = messageText });
        }
    }
 
    private ItemId ExtractItemId(string TagXmlNode Node)
    {
        // Extract an ItemId from the node
        string itemId = "";
        string changeKey = "";
 
        foreach (XmlNode node in Node.ChildNodes)
        {
            if (node.LocalName == Tag)
            {
                // This is our ItemId
                itemId = node.Attributes["Id"].Value;
                changeKey = node.Attributes["ChangeKey"].Value;
                break;
            }
        }
        if (!String.IsNullOrEmpty(itemId))
        {
            ItemId id = new ItemId(itemId);
            //id.ChangeKey = changeKey;
            return id;
        }
        return null;
    }
 
    /// <summary>
    /// XML에서 FolderId 가져오기
    /// </summary>
    /// <param name="Tag"></param>
    /// <param name="Node"></param>
    /// <returns></returns>
    private FolderId ExtractFolderId(string TagXmlNode Node)
    {
        string folderId = "";
        string changeKey = "";
 
        foreach (XmlNode node in Node.ChildNodes)
        {
            if (node.LocalName == Tag)
            {
                //ItemId
                folderId = node.Attributes["Id"].Value;
                changeKey = node.Attributes["ChangeKey"].Value;
                break;
            }
        }
 
        if (!String.IsNullOrEmpty(folderId))
        {
            FolderId id = new FolderId(folderId);
            //id.ChangeKey = changeKey;
            return id;
        }
        return null;
    }
 
    /// <summary>
    /// XML에서 워터마크 가져오기
    /// </summary>
    /// <param name="Node">XML</param>
    /// <returns></returns>
    public string GetWatermark(XmlNode Node)
    {
        string returnValue = string.Empty;
 
        foreach (XmlNode node in Node.ParentNode.ChildNodes)
        {
            if (node.LocalName == "StatusEvent")
            {
                foreach (XmlNode n in node.ChildNodes)
                {
                    if (n.LocalName == "Watermark")
                    {
                        returnValue = n.Value;
                    }
                }
            }
        }
 
        return returnValue;
    }
 
    /// <summary>
    /// XML에서 이전 워터마크 가져오기
    /// </summary>
    /// <param name="Node">XML</param>
    /// <returns></returns>
    public string GetPreviousWatermark(XmlNode Node)
    {
        string returnValue = string.Empty;
 
        foreach (XmlNode node in Node.ParentNode.ChildNodes)
        {
            if (node.LocalName == "PreviousWatermark")
            {
                returnValue = node.InnerText;
            }
        }
 
        return returnValue;
    }
 
    private void Log(string Detailsstring Description = "")
    {
        try
        {
            if (_Logger == nullreturn;
            _Logger.Log(DetailsDescription);
        }
        catch { }
    }
 
    public void Close()
    {
        // Release the listener
        try
        {
            _Listener.Stop();
            _Listener.Close();
        }
        catch { }
    }
}

[코드1]Listener의 전체 소스


 위 소스에서 XML을 파싱하는 부분의 코드는 EWS의 기본 로직에 따라 해당하는 ListenEventArgs를 생성하고 있다. 예로 XML 파싱에서 MovedEvents가 발생되었다고 판독이 되면 ItemId가 있는지 체크하고 있으면 기존 아이템처럼 수행하면 되고 아니면 Folder 처럼 동작하도록 되었다. Exchagne 에서는 모든 아이템이 동등한 객체처럼 관리가 되고 있기 때문이다. Folder가 옮겨져도 MovedEvents가 발생하고 메일이 옮겨져도 MovedEvents가 발생하기 때문이다. 다른 아이템도 마찬가지로 이벤트가 동작하게 되어 있다.


 이번 "코드1"의 소스 길이가 다른 사항에 비해 상단히 길게 작성이 되어 있다. 쉽게 이해할 수 없을 수도 있으나 디버깅이나 실행 흐름을 따라가다가 보면 어렵지 않게 이해할 수 있으리라 기대한다. 다만 조급히 생각하지 않고 시간 투자만 한다면 어렵지 않을것이다. 다만 이곳에서 직접적으로 보여줄 수 없는 부분이 서버에 대한 정보라고 할수 있을 것이다. ConfigurationManager을 통해서 설정 파일에서 읽어 오는 부분은 옆에 주석으로 해당 값에 대한 예시를 같이 적어 두었으니 구동 환경에 맞게 수정하면 될 것이다. 그리고 여기에 리소스로 포함되는 XML 파일을 보여주는 것으로 이번 Push Notification을 마치도록 하겠다.


<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <m:SendNotificationResult>
      <m:SubscriptionStatus>OK</m:SubscriptionStatus>
    </m:SendNotificationResult>
  </soap:Body>
</soap:Envelope>

[코드2] 리소스에 포함되는 XML 파일 ( MBX에 정상적으로 수신되었다고 리턴 값을 보내는 정보)








Notifications - Push, Pull, Stream Notification #4


 Exchange 2013을 기반으로 개발을 진행하면서 맞닥뜨리게 된 케이스에 대해서 공유 하고자 한다. 이전에 잠깐 EWS(Exchange Web Service)를 통해 간단한 기능을 사용해본 경험이 전부라서 하나의 기능을 구현하기 위해 여러가지 방안에 대해서 바위에 계란치기로 직접 부딪혀 볼 수 밖에 없었다. 이런 힘들고 시간 싸움을 줄이는데 도움을 드리는데 일조 하고자 이곳에 공유 하고자 합니다. 비록 덜 정제 되고 문서 미비할 수 있지만 참고 사항으로 알아 두셨으면 합니다. 



 이번에는 지난 시간에 이어 Push와 Pull에 대해서 알아 보도록 하겠습니다. Push 방식은 Pull과 Stream 방식과는 다른 방식으로 MBX에서 이벤트를 보내주고 Listener에서 받아 주는 구조로 되어 있기 때문에 구독할 수 있는 별도의 서비스가 필요 합니다. "그림1" 를 보면 3번 스텝에 해당하는 서비스로 데이터를 받아서 처리를 하는 구조입니다.

구조에 따라 CAS 서버가 Register와 Listener 모두를 같이 담당할 수도 있고 별도의 서버로 구성을 하셔도 됩니다. 서버의 스펙과 사용자 수에 따라 조율하면 되겠습니다.


[그림1] Pull notification data flow



2. Push notifications

 "코드1"은 비 동기로 Push 이벤트 구독을 신청한다. "그림1"에서의 1번과 2번 과정을 수행하는 것이다.

////비동기를 관리할 수 있게 함 List<System.Threading.Tasks.Task> tasks = new List<System.Threading.Tasks.Task>(); #region Push notification test // Create our listener // "그림1"에서 3번 수행에 필요한 Listener를 활성화 시켜 줌 _listener = new PushConsoleListener(_logger);
_listener.EventReceived += new PushConsoleListener.ListenEventHandler(_listener_EventReceived); //사용자 정보를 가져와서 Push 이벤트 등록 신청 한다. foreach (var member in Members.GetUsers()) {     var m = member;     var task = System.Threading.Tasks.Task.Factory.StartNew(() =>     {         try         {             //Push 이벤트 구독 신청             SubscriptionAdd(m);         }         catch (Exception ex)         {             Log.WriteLine(string.Format("{0}, {1}""SubscriptionAdd"ex.Message));         }     });     tasks.Add(task); } #endregion //모든 비동기 겍체가 완료가 될때까지 대기 함. System.Threading.Tasks.Task.WaitAll(tasks.ToArray());

[코드1] Push Event 구독 요청


 "코드2"번은 "그림1"에서 2번에 해당하는 작업을 수행한다.

/// <summary> /// Push 이벤트 구독 신청 /// </summary> /// <param name="address">사용자 ID - 이메일 주소</param> /// <param name="waterMark">워터 마크 - 특정 시점의 item을 가져올 수 있음</param>

public void SubscriptionAdd(string addressstring waterMark = null) {     Console.WriteLine("{0} 등록중"address);     //인증된 ExchangeService instance 가져오기     ExchangeService _service = EWSHelper.GetService();     var folderIds = new List<FolderId>()                 {                     WellKnownFolderName.Inbox                 };     var eventTypes = new List<EventType>();     //NewMail에 대해서만 이벤트를 받음 처리     eventTypes.Add(EventType.NewMail);  

//임시적으로 waterMark를 null로 할당( 이 기능으로 특정 시점의 item을 가져올 수 있음 ) //var waterMark = null;
    _service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddressaddress); //옵션중에 frequency는 1분에 한번씩 Listener이 살아 있는지 체크하도록 하는 옵션이다. 1분부터 1440분까지 옵션을 줄 수가 있다.     PushSubscription pushSubscription = _service.SubscribeToPushNotifications(folderIdsnew Uri(_listener.URi), 1waterMarkeventTypes.ToArray());     memberAddress.TryAdd(pushSubscription.Idaddress);     Console.WriteLine(address + " push listener 등록 됨"); }

[코드2] Push 이벤트 구독 등록


 "코드1"과 "코드2"에서와 같이 MBX에 Push event 구독을 신청하였으면 이제 Listener을 할 수 있는 역할을 하는 서비스를 세팅해야 한다. ( "코드1"에서 해당 클래스를 활성화 시켜 주는 작업은 이미 수행 되었다. )


Push 이벤트는 MBX에서 XML 형식으로 Listener에 값을 넘겨 준다. 그러므로 XML에서 원하는 데이타를 추출하기 위해 XML Parser를 이용해서 작업을 하고 EventArgs를 만들어서 EventHandler를 넘겨 Event를 통해 처리하도록 하고 있다. 아래는 MBX에서 보내주는 XML 전문이다. 

<!-- 정상적인 메시지 -->
<?xml version="1.0" encoding="utf-8"?>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
  <soap11:Header>
    <t:RequestServerVersion xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" Version="Exchange2013" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" />
  </soap11:Header>
  <soap11:Body>
    <m:SendNotification xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
      <m:ResponseMessages>
        <m:SendNotificationResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Notification>
            <t:SubscriptionId>DwBtbWJ4MDIuYnBpby5uZXQQAAAA8D/AEWNefUirlcJW7vLT2zE/ABKs3s8I</t:SubscriptionId>
            <t:PreviousWatermark>AQAAAKGzfQd17kZIovKBge0oWhrMcAAAAAAAAAE=</t:PreviousWatermark>
            <t:MoreEvents>false</t:MoreEvents>
            <t:StatusEvent>
              <t:Watermark>AQAAAKGzfQd17kZIovKBge0oWhrMcAAAAAAAAAE=</t:Watermark>
            </t:StatusEvent>
          </m:Notification>
        </m:SendNotificationResponseMessage>
      </m:ResponseMessages>
    </m:SendNotification>
  </soap11:Body>
</soap11:Envelope>

[코드3] MBX가 Listener에게 보내주는 XML 정보 (정상)


<!-- 비 정상적인 메시지 -->
<?xml version="1.0" encoding="utf-8"?>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
  <soap11:Header>
    <t:RequestServerVersion xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" Version="Exchange2013" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" />
  </soap11:Header>
  <soap11:Body>
    <m:SendNotification xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
      <m:ResponseMessages>
        <m:SendNotificationResponseMessage ResponseClass="Error">
          <m:MessageText>이 구독의 이벤트를 검색할 수 없습니다. 구독을 다시 만들어야 합니다.</m:MessageText>
          <m:ResponseCode>ErrorReadEventsFailed</m:ResponseCode>
          <m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>
          <m:MessageXml>
            <t:Value Name="SubscriptionId">DwBtbWJ4MDEuYnBpby5uZXQQAAAAPxYnRYDXgkCvXyhllmZHPpV2k0Sr3s8I</t:Value>
          </m:MessageXml>
        </m:SendNotificationResponseMessage>
      </m:ResponseMessages>
    </m:SendNotification>
  </soap11:Body>
</soap11:Envelope>

[코드4]MBX가 Listener에게 보내주는 XML 정보 (비 정상)


 이제 XML 파일을 파싱해서 얻어진 정보를 EventArgs로 사용하는 클래스를 먼저 살펴 보자. 이 클래스를 통해 Listener에 연결된 핸들러에서 편리하게 해당 정보를 얻어 사용할 수 있다. 

/// <summary>
/// EWS 2.0, Exchange 2013, Push notification 관련 XML에 관계된 EventArgs
/// </summary>
public class ListenEventArgs : EventArgs
{
    #region fields
    private string _xml = "";
    private string _subscriptionId = "";
    private EventType _eventtype = EventType.Status;
    private ItemId _itemId = new ItemId(new Guid().ToString());
    private FolderId _folderId = null;
    private FolderId _parentFolderId = null;
    private ItemId _oldItemId = null;
    private FolderId _oldFolderId = null;
    private FolderId _oldParentFolderId = null;
    #endregion
 
    #region Constructor
    /// <summary>
    /// XML 데이터를 가지고 인스턴스 생성
    /// </summary>
    /// <param name="XML">XML 데이타</param>
    public ListenEventArgs(string XML)
    {
        //XML에 notification 상세 정보가 있다.
        _xml = XML;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId">구독 요청시 생성된 ID값</param>
    /// <param name="isError">에러여부</param>
    public ListenEventArgs(string SubscriptionIdbool isError)
    {
        this._subscriptionId = SubscriptionId;
        this.IsError = isError;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId">구독 요청시 생성된 ID값</param>
    /// <param name="EventType">이벤트 타입</param>
    /// <param name="ItemId">ItemId</param>
    /// <param name="ParentFolderId">ParentFolderId</param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeItemId ItemIdFolderId ParentFolderId)
    {
        _subscriptionId = SubscriptionId;
        _eventtype = EventType;
        _itemId = ItemId;
        _parentFolderId = ParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="ItemId"></param>
    /// <param name="ParentFolderId"></param>
    /// <param name="OldItemId"></param>
    /// <param name="OldParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeItemId ItemIdFolderId ParentFolderIdItemId OldItemIdFolderId OldParentFolderId)
        : this(SubscriptionIdEventTypeItemIdParentFolderId)
    {
        _oldItemId = OldItemId;
        _oldParentFolderId = OldParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="FolderId"></param>
    /// <param name="ParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeFolderId FolderIdFolderId ParentFolderId)
    {
        _subscriptionId = SubscriptionId;
        _eventtype = EventType;
        _folderId = FolderId;
        _parentFolderId = ParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="FolderId"></param>
    /// <param name="ParentFolderId"></param>
    /// <param name="OldFolderId"></param>
    /// <param name="OldParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeFolderId FolderIdFolderId ParentFolderIdFolderId OldFolderIdFolderId OldParentFolderId)
        : this(SubscriptionIdEventTypeFolderIdParentFolderId)
    {
        _oldFolderId = OldFolderId;
        _oldParentFolderId = OldParentFolderId;
    }
    #endregion
 
    /// <summary>
    /// Push 구독 요청시 반환된 값
    /// </summary>
    public string SubscriptionId
    {
        get { return _subscriptionId; }
    }
 
    /// <summary>
    /// 이벤트 타입
    /// </summary>
    public EventType EventType
    {
        get { return _eventtype; }
    }
 
    /// <summary>
    /// Item unique Id
    /// </summary>
    public ItemId ItemId
    {
        get { return _itemId; }
    }
 
    /// <summary>
    /// Folder Id
    /// </summary>
    public FolderId FolderId
    {
        get { return _folderId; }
    }
 
    /// <summary>
    /// Parent folder Id
    /// </summary>
    public FolderId ParentFolderId
    {
        get { return _parentFolderId; }
    }
 
    /// <summary>
    /// Old Item Id
    /// </summary>
    public ItemId OldItemId
    {
        get { return _oldItemId; }
    }
 
    /// <summary>
    /// Old Parent Folder Id
    /// </summary>
    public FolderId OldParentFolderId
    {
        get { return _oldParentFolderId; }
    }
 
    /// <summary>
    /// MBX 주소
    /// </summary>
    public string RemortEndpoint { getset; }
 
    /// <summary>
    /// 워터마크
    /// </summary>
    public string Watermark { getset; }
 
    /// <summary>
    /// 이전 워터마크
    /// </summary>
    public string PreviousWatermark { getset; }
 
    /// <summary>
    /// 에러여부
    /// </summary>
    private bool isError = false;
 
    /// <summary>
    /// 에러여부
    /// </summary>
    public bool IsError
    {
        get
        {
            return isError;
        }
        set
        {
            isError = value;
        }
    }
 
    /// <summary>
    /// Push server에서 보낸 준 XML Data
    /// </summary>
    public string Xml
    {
        get { return _xml; }
        set { _xml = value; }
    }
}

[코드5] XML에서 파싱된 정보를 저장하는 클래스


 다음 포스트에서 Listener에서 XML을 파싱하는 클래스부터 다뤄보도록 하겠다.

Notifications - Push, Pull, Stream Notification #3


 Exchange 2013을 기반으로 개발을 진행하면서 맞닥뜨리게 된 케이스에 대해서 공유 하고자 한다. 이전에 잠깐 EWS(Exchange Web Service)를 통해 간단한 기능을 사용해본 경험이 전부라서 하나의 기능을 구현하기 위해 여러가지 방안에 대해서 바위에 계란치기로 직접 부딪혀 볼 수 밖에 없었다. 이런 힘들고 시간 싸움을 줄이는데 도움을 드리는데 일조 하고자 이곳에 공유 하고자 합니다. 비록 덜 정제 되고 문서 미비할 수 있지만 참고 사항으로 알아 두셨으면 합니다. 



 이번 포스트에서는 지난 시간에 다루지 못했던 프로그래머 관점에서 살펴 보고자 한다. Pull, Push, Stream 방식은 가각의 장단점이 있으니 적절히 판단하여 실 환경에 적용해야 하겠습니다. 각 장단점은 이전 포스트의 첫번째에서 비교를 해 놓았습니다.


1. Stream notifications

#region Streaming notifications test ExchangeService serviceConnection = EWSHelper.GetService();   //EWS를 반환 한다. StreamingSubscriptionConnection streamConnection = new StreamingSubscriptionConnection(serviceConnection30);  //Timeout은 1 ~ 30분, 하나의 커넥션을 만든다. FolderId[] foldersToWatch = null; StreamingSubscription streamingsubscription = null; foreach (var member in Members.GetUsers()) {     //비동기 상에서 overwrap이 되지 않도록 처리     var m = member;     var task = System.Threading.Tasks.Task.Factory.StartNew(() =>     {         Log.WriteLine(m + " : Subscription 등록중");         #region Server와 Impersonate를 진행         ExchangeService service = EWSHelper.GetService();   //GetService(member);         //가장할 사용자 정보         service.ImpersonatedUserId = new ImpersonatedUserId() { Id = mIdType = ConnectingIdType.SmtpAddress }; //구독에 필요한 정보를 세팅         StreamingSubscription streamSubscription = service.SubscribeToStreamingNotifications(                     new FolderId[] { WellKnownFolderName.Inbox },                     EventType.NewMail,                     EventType.Modified             );         //connection에 subscription를 추가하여 이벤트를 받을 수 있도록 함. //하나의 커넥션의 여러개의 Subscription을 추가한다. 기본적으로 StreamConnection은 2000를 넘으면 서버에서 Busy Exception을 발생시킨다.         streamConnection.AddSubscription(streamSubscription);         Log.WriteLine(m + " : Subscription 등록 완료");     });     tasks.Add(task); } #endregion //모든 비동기 겍체가 완료가 될때까지 대기 함. System.Threading.Tasks.Task.WaitAll(tasks.ToArray()); //Connections.TryAdd("Connection1", streamConnection); // 이벤트 등록 streamConnection.OnNotificationEvent += streamConnection_OnNotificationEvent; streamConnection.OnSubscriptionError += streamConnection_OnSubscriptionError; streamConnection.OnDisconnect += streamConnection_OnDisconnect; streamConnection.Open();

[코드1] Stream notification을 등록 요청 및 이벤트 핸들러 등록



 아래 "코드2"에서는 위에서 등록한 이벤트 핸들러에 대한 코드다.

/// <summary> /// Subscription 설정에 따른 Notification 이벤트 발생 /// </summary> /// <param name="sender"></param> /// <param name="args"></param> private void streamConnection_OnNotificationEvent(object senderNotificationEventArgs args) {     var task = System.Threading.Tasks.Task.Factory.StartNew(() =>         {             var message = string.Empty;             var id = args.Subscription.Service.ImpersonatedUserId.Id;             message = id + ";";             #region Binding된 데이터를 가지고 테스트             IEnumerable<ItemId> itemEvents = from e in args.Events.OfType<ItemEvent>()                                                 select e.ItemId;             //Item.Bind(args.Subscription.Service, itemId)   //하나의 객체에 대해서만 바인딩             var response = args.Subscription.Service.BindToItems(itemEventsnew PropertySet(BasePropertySet.IdOnlyEmailMessageSchema.From));             var items = response.Select(itemResponse => itemResponse.Item);             foreach (var item in items)             {                 EmailMessage msg = item as EmailMessage;                 if (msg != null)                 {                     message += msg.From.Address + "," + item.Id + "," + DateTime.Now.ToString() + ",watermark=" + args.Subscription.Watermark;                 }             }             #endregion 

            Log.WriteLine(message);         }); } /// <summary> /// Subscription 관련 에러 이벤트 발생 /// </summary> /// <param name="sender"></param> /// <param name="args"></param> void streamConnection_OnSubscriptionError(object senderSubscriptionErrorEventArgs args) {              } /// <summary> /// Subscription Connection 설정 시간(최대 30분)이 지나서 Disconnect 이벤트 발생하면,  Connection 재 연결 처리 /// </summary> /// <param name="sender"></param> /// <param name="args"></param> void streamConnection_OnDisconnect(object senderSubscriptionErrorEventArgs args) {     StreamingSubscriptionConnection connection = (StreamingSubscriptionConnection)sender;                  if (connection != null)     {         if (!connection.IsOpen)         {             //Connection이 끊어지면 다시 오픈하여 이벤트를 받을 수 있도록 함.             connection.Open();         }     } }

[코드2] 이벤트 핸들러


 위 코드에서 streamConnection_OnDisconnection 이벤트는 StreamConnection이 Timeout인해(등록시 30분으로 세팅하여 연결하였고 30분 지나면 OnDisconnection 발생 후 종료 한다) 연결이 끊어진다. 그래서 이 이벤트가 발생하면 계속 유지가 되도록 코딩이 되어 있다. OnNotificationEvent는 일반적인 시나리오에서 발생하는 이벤트이며 이곳에서 아이템에 대한 상태를 판단할 수 있다. 그리고 더 많은 정보를 가져오기 위해서는 Bind를 통해서 한번 더 서버에 접근해야 추가 정보를 가져올 수 있다.












Exchange flow diagram V1.pptx


+ Recent posts