PageRenderTime 26ms CodeModel.GetById 14ms app.highlight 7ms RepoModel.GetById 1ms app.codeStats 0ms

/SignalR.Client/Connection.cs

https://github.com/kpmrafeeq/SignalR
C# | 449 lines | 294 code | 60 blank | 95 comment | 37 complexity | 6eb341114c4eff0edfe72c5b05b5224a MD5 | raw file
  1using System;
  2using System.Collections.Concurrent;
  3using System.Collections.Generic;
  4using System.Globalization;
  5using System.Linq;
  6using System.Net;
  7using System.Reflection;
  8using System.Threading.Tasks;
  9using Newtonsoft.Json;
 10using Newtonsoft.Json.Linq;
 11using SignalR.Client.Http;
 12using SignalR.Client.Transports;
 13
 14namespace SignalR.Client
 15{
 16    /// <summary>
 17    /// Provides client connections for SignalR services.
 18    /// </summary>
 19    public class Connection : IConnection
 20    {
 21        private static Version _assemblyVersion;
 22
 23        private IClientTransport _transport;
 24
 25        // The default connection state is disconnected
 26        private ConnectionState _state = ConnectionState.Disconnected;
 27
 28        // Used to synchornize state changes
 29        private readonly object _stateLock = new object();
 30
 31        /// <summary>
 32        /// Occurs when the <see cref="Connection"/> has received data from the server.
 33        /// </summary>
 34        public event Action<string> Received;
 35
 36        /// <summary>
 37        /// Occurs when the <see cref="Connection"/> has encountered an error.
 38        /// </summary>
 39        public event Action<Exception> Error;
 40
 41        /// <summary>
 42        /// Occurs when the <see cref="Connection"/> is stopped.
 43        /// </summary>
 44        public event Action Closed;
 45
 46        /// <summary>
 47        /// Occurs when the <see cref="Connection"/> successfully reconnects after a timeout.
 48        /// </summary>
 49        public event Action Reconnected;
 50
 51        /// <summary>
 52        /// Occurs when the <see cref="Connection"/> state changes.
 53        /// </summary>
 54        public event Action<StateChange> StateChanged;
 55
 56        /// <summary>
 57        /// Initializes a new instance of the <see cref="Connection"/> class.
 58        /// </summary>
 59        /// <param name="url">The url to connect to.</param>
 60        public Connection(string url)
 61            : this(url, (string)null)
 62        {
 63        }
 64
 65        /// <summary>
 66        /// Initializes a new instance of the <see cref="Connection"/> class.
 67        /// </summary>
 68        /// <param name="url">The url to connect to.</param>
 69        /// <param name="queryString">The query string data to pass to the server.</param>
 70        public Connection(string url, IDictionary<string, string> queryString)
 71            : this(url, CreateQueryString(queryString))
 72        {
 73
 74        }
 75
 76        /// <summary>
 77        /// Initializes a new instance of the <see cref="Connection"/> class.
 78        /// </summary>
 79        /// <param name="url">The url to connect to.</param>
 80        /// <param name="queryString">The query string data to pass to the server.</param>
 81        public Connection(string url, string queryString)
 82        {
 83            if (url.Contains("?"))
 84            {
 85                throw new ArgumentException("Url cannot contain QueryString directly. Pass QueryString values in using available overload.", "url");
 86            }
 87
 88            if (!url.EndsWith("/"))
 89            {
 90                url += "/";
 91            }
 92
 93            Url = url;
 94            QueryString = queryString;
 95            Groups = Enumerable.Empty<string>();
 96            Items = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
 97            State = ConnectionState.Disconnected;
 98        }
 99
100        /// <summary>
101        /// Gets or sets the cookies associated with the connection.
102        /// </summary>
103        public CookieContainer CookieContainer { get; set; }
104
105        /// <summary>
106        /// Gets or sets authentication information for the connection.
107        /// </summary>
108        public ICredentials Credentials { get; set; }
109
110        /// <summary>
111        /// Gets or sets the groups for the connection.
112        /// </summary>
113        public IEnumerable<string> Groups { get; set; }
114
115        /// <summary>
116        /// Gets the url for the connection.
117        /// </summary>
118        public string Url { get; private set; }
119
120        /// <summary>
121        /// Gets or sets the last message id for the connection.
122        /// </summary>
123        public string MessageId { get; set; }
124
125        /// <summary>
126        /// Gets or sets the connection id for the connection.
127        /// </summary>
128        public string ConnectionId { get; set; }
129
130        /// <summary>
131        /// Gets a dictionary for storing state for a the connection.
132        /// </summary>
133        public IDictionary<string, object> Items { get; private set; }
134
135        /// <summary>
136        /// Gets the querystring specified in the ctor.
137        /// </summary>
138        public string QueryString { get; private set; }
139
140        /// <summary>
141        /// Gets the current <see cref="ConnectionState"/> of the connection.
142        /// </summary>
143        public ConnectionState State
144        {
145            get
146            {
147                lock (_stateLock)
148                {
149                    return _state;
150                }
151            }
152            private set
153            {
154                if (_state != value)
155                {
156                    if (StateChanged != null)
157                    {
158                        StateChanged(new StateChange(_state, value));
159                    }
160
161                    _state = value;
162                }
163            }
164        }
165
166        /// <summary>
167        /// Starts the <see cref="Connection"/>.
168        /// </summary>
169        /// <returns>A task that represents when the connection has started.</returns>
170        public Task Start()
171        {
172            return Start(new DefaultHttpClient());
173        }
174
175        /// <summary>
176        /// Starts the <see cref="Connection"/>.
177        /// </summary>
178        /// <param name="httpClient">The http client</param>
179        /// <returns>A task that represents when the connection has started.</returns>
180        public Task Start(IHttpClient httpClient)
181        {
182            // Pick the best transport supported by the client
183            return Start(new AutoTransport(httpClient));
184        }
185
186        /// <summary>
187        /// Starts the <see cref="Connection"/>.
188        /// </summary>
189        /// <param name="transport">The transport to use.</param>
190        /// <returns>A task that represents when the connection has started.</returns>
191        public virtual Task Start(IClientTransport transport)
192        {
193            if (!ChangeState(ConnectionState.Disconnected, ConnectionState.Connecting))
194            {
195                return TaskAsyncHelper.Empty;
196            }
197
198            _transport = transport;
199
200            return Negotiate(transport);
201        }
202
203        protected virtual string OnSending()
204        {
205            return null;
206        }
207
208        private Task Negotiate(IClientTransport transport)
209        {
210            var negotiateTcs = new TaskCompletionSource<object>();
211
212            transport.Negotiate(this).Then(negotiationResponse =>
213            {
214                VerifyProtocolVersion(negotiationResponse.ProtocolVersion);
215
216                ConnectionId = negotiationResponse.ConnectionId;
217
218                var data = OnSending();
219                StartTransport(data).ContinueWith(negotiateTcs);
220            })
221            .ContinueWithNotComplete(negotiateTcs);
222
223            var tcs = new TaskCompletionSource<object>();
224            negotiateTcs.Task.ContinueWith(task =>
225            {
226                try
227                {
228                    // If there's any errors starting then Stop the connection                
229                    if (task.IsFaulted)
230                    {
231                        Stop();
232                        tcs.SetException(task.Exception.Unwrap());
233                    }
234                    else if (task.IsCanceled)
235                    {
236                        Stop();
237                        tcs.SetCanceled();
238                    }
239                    else
240                    {
241                        tcs.SetResult(null);
242                    }
243                }
244                catch (Exception ex)
245                {
246                    tcs.SetException(ex);
247                }
248            },
249            TaskContinuationOptions.ExecuteSynchronously);
250
251            return tcs.Task;
252        }
253
254        private Task StartTransport(string data)
255        {
256            return _transport.Start(this, data)
257                             .Then(() =>
258                             {
259                                 ChangeState(ConnectionState.Connecting, ConnectionState.Connected);
260                             });
261        }
262
263        private bool ChangeState(ConnectionState oldState, ConnectionState newState)
264        {
265            return ((IConnection)this).ChangeState(oldState, newState);
266        }
267
268        bool IConnection.ChangeState(ConnectionState oldState, ConnectionState newState)
269        {
270            lock (_stateLock)
271            {
272                // If we're in the expected old state then change state and return true
273                if (_state == oldState)
274                {
275                    State = newState;
276                    return true;
277                }
278
279                // Invalid transition
280                return false;
281            }
282        }
283
284        private static void VerifyProtocolVersion(string versionString)
285        {
286            Version version;
287            if (String.IsNullOrEmpty(versionString) ||
288                !TryParseVersion(versionString, out version) ||
289                !(version.Major == 1 && version.Minor == 0))
290            {
291                throw new InvalidOperationException("Incompatible protocol version.");
292            }
293        }
294
295        /// <summary>
296        /// Stops the <see cref="Connection"/>.
297        /// </summary>
298        public virtual void Stop()
299        {
300            try
301            {
302                // Do nothing if the connection is offline
303                if (State == ConnectionState.Disconnected)
304                {
305                    return;
306                }
307
308                _transport.Stop(this);
309
310                if (Closed != null)
311                {
312                    Closed();
313                }
314            }
315            finally
316            {
317                State = ConnectionState.Disconnected;
318            }
319        }
320
321        /// <summary>
322        /// Sends data asynchronously over the connection.
323        /// </summary>
324        /// <param name="data">The data to send.</param>
325        /// <returns>A task that represents when the data has been sent.</returns>
326        public Task Send(string data)
327        {
328            return ((IConnection)this).Send<object>(data);
329        }
330
331        /// <summary>
332        /// Sends an object that will be JSON serialized asynchronously over the connection.
333        /// </summary>
334        /// <param name="value">The value to serialize.</param>
335        /// <returns>A task that represents when the data has been sent.</returns>
336        public Task Send(object value)
337        {
338            return Send(JsonConvert.SerializeObject(value));
339        }
340
341        Task<T> IConnection.Send<T>(string data)
342        {
343            if (State == ConnectionState.Disconnected)
344            {
345                throw new InvalidOperationException("Start must be called before data can be sent.");
346            }
347
348            if (State == ConnectionState.Connecting)
349            {
350                throw new InvalidOperationException("The connection has not been established.");
351            }
352
353            return _transport.Send<T>(this, data);
354        }
355
356        void IConnection.OnReceived(JToken message)
357        {
358            OnReceived(message);
359        }
360
361        protected virtual void OnReceived(JToken message)
362        {
363            if (Received != null)
364            {
365                Received(message.ToString());
366            }
367        }
368
369        void IConnection.OnError(Exception error)
370        {
371            if (Error != null)
372            {
373                Error(error);
374            }
375        }
376
377        void IConnection.OnReconnected()
378        {
379            if (Reconnected != null)
380            {
381                Reconnected();
382            }
383        }
384
385        void IConnection.PrepareRequest(IRequest request)
386        {
387#if WINDOWS_PHONE
388            // http://msdn.microsoft.com/en-us/library/ff637320(VS.95).aspx
389            request.UserAgent = CreateUserAgentString("SignalR.Client.WP7");
390#else
391#if SILVERLIGHT
392            // Useragent is not possible to set with Silverlight, not on the UserAgent property of the request nor in the Headers key/value in the request
393#else
394            request.UserAgent = CreateUserAgentString("SignalR.Client");
395#endif
396#endif
397            if (Credentials != null)
398            {
399                request.Credentials = Credentials;
400            }
401
402            if (CookieContainer != null)
403            {
404                request.CookieContainer = CookieContainer;
405            }
406        }
407
408        private static string CreateUserAgentString(string client)
409        {
410            if (_assemblyVersion == null)
411            {
412#if NETFX_CORE
413                _assemblyVersion = new Version("0.5.0.0");
414#else
415                _assemblyVersion = new AssemblyName(typeof(Connection).Assembly.FullName).Version;
416#endif
417            }
418
419#if NETFX_CORE
420            return String.Format(CultureInfo.InvariantCulture, "{0}/{1} ({2})", client, _assemblyVersion, "Unknown OS");
421#else
422            return String.Format(CultureInfo.InvariantCulture, "{0}/{1} ({2})", client, _assemblyVersion, Environment.OSVersion);
423#endif
424        }
425
426        private static bool TryParseVersion(string versionString, out Version version)
427        {
428#if WINDOWS_PHONE || NET35
429            try
430            {
431                version = new Version(versionString);
432                return true;
433            }
434            catch
435            {
436                version = null;
437                return false;
438            }
439#else
440            return Version.TryParse(versionString, out version);
441#endif
442        }
443
444        private static string CreateQueryString(IDictionary<string, string> queryString)
445        {
446            return String.Join("&", queryString.Select(kvp => kvp.Key + "=" + kvp.Value).ToArray());
447        }
448    }
449}