From 52a4b792b5f2774b73049a680a669f2603e4caad Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Thu, 13 Feb 2020 00:10:50 +0000 Subject: [PATCH 1/9] Add WebSocket --- src/Mozilla.IoT.WebThing/ActionCollection.cs | 39 ++++ src/Mozilla.IoT.WebThing/ActionContext.cs | 2 +- src/Mozilla.IoT.WebThing/Context.cs | 3 + .../Endpoints/PostAction.cs | 2 +- .../Endpoints/PostActions.cs | 2 +- .../Endpoints/PutProperty.cs | 6 +- src/Mozilla.IoT.WebThing/EventCollection.cs | 7 + .../IEndpointRouteBuilderExtensions.cs | 4 +- .../Extensions/IServiceExtensions.cs | 29 +++ .../Mozilla.IoT.WebThing.csproj | 2 +- .../WebSockets/AddEventSubscription.cs | 43 +++++ .../WebSockets/IWebSocketAction.cs | 16 ++ .../WebSockets/RequestAction.cs | 58 ++++++ .../WebSockets/SetThingProperty.cs | 26 +++ .../WebSockets/WebSocket.cs | 166 ++++++++++++++++++ 15 files changed, 397 insertions(+), 8 deletions(-) create mode 100644 src/Mozilla.IoT.WebThing/ActionCollection.cs create mode 100644 src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs create mode 100644 src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs create mode 100644 src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs create mode 100644 src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs create mode 100644 src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs diff --git a/src/Mozilla.IoT.WebThing/ActionCollection.cs b/src/Mozilla.IoT.WebThing/ActionCollection.cs new file mode 100644 index 0000000..d861182 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/ActionCollection.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Mozilla.IoT.WebThing.Actions; + +namespace Mozilla.IoT.WebThing +{ + public class ActionCollection : IEnumerable + { + private readonly ConcurrentDictionary _actions; + + public event EventHandler Added; + + public ActionCollection() + { + _actions = new ConcurrentDictionary(); + } + + public void Add(Guid id, ActionInfo actionInfo) + { + _actions.TryAdd(id, actionInfo); + + var added = Added; + added?.Invoke(this, actionInfo); + } + + public bool TryGetValue(Guid id, out ActionInfo action) + => _actions.TryGetValue(id, out action); + + public bool TryRemove(Guid id, out ActionInfo action) + => _actions.TryRemove(id, out action); + + public IEnumerator GetEnumerator() + => _actions.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Mozilla.IoT.WebThing/ActionContext.cs b/src/Mozilla.IoT.WebThing/ActionContext.cs index 1c2e1d2..a6c2969 100644 --- a/src/Mozilla.IoT.WebThing/ActionContext.cs +++ b/src/Mozilla.IoT.WebThing/ActionContext.cs @@ -12,6 +12,6 @@ public ActionContext(Type actionType) } public Type ActionType { get; } - public ConcurrentDictionary Actions { get; } = new ConcurrentDictionary(); + public ActionCollection Actions { get; } = new ActionCollection(); } } diff --git a/src/Mozilla.IoT.WebThing/Context.cs b/src/Mozilla.IoT.WebThing/Context.cs index 41e5cf2..4a6e5f7 100644 --- a/src/Mozilla.IoT.WebThing/Context.cs +++ b/src/Mozilla.IoT.WebThing/Context.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Net.WebSockets; using Mozilla.IoT.WebThing.Converts; namespace Mozilla.IoT.WebThing @@ -23,5 +25,6 @@ public Context(IThingConverter converter, public Dictionary Events { get; } public Dictionary Actions { get; } + public ConcurrentDictionary Sockets { get; } = new ConcurrentDictionary(); } } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs b/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs index b2cb861..31f7093 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs @@ -77,7 +77,7 @@ public static async Task InvokeAsync(HttpContext context) actionInfo.ExecuteAsync(thing, service) .ConfigureAwait(false); - actionContext.Actions.TryAdd(actionInfo.Id, actionInfo); + actionContext.Actions.Add(actionInfo.Id, actionInfo); } if (actionsToExecute.Count == 1) diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs b/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs index 854d3ff..f31c531 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs @@ -68,7 +68,7 @@ public static async Task InvokeAsync(HttpContext context) actionInfo.ExecuteAsync(thing, service) .ConfigureAwait(false); - thing.ThingContext.Actions[actionInfo.GetActionName()].Actions.TryAdd(actionInfo.Id, actionInfo); + thing.ThingContext.Actions[actionInfo.GetActionName()].Actions.Add(actionInfo.Id, actionInfo); } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs index 03ed03d..7ceecb4 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs @@ -32,12 +32,12 @@ public static async Task InvokeAsync(HttpContext context) var property = context.GetRouteData("property"); - logger.LogTrace("Going to set property {propertyName}", property); + logger.LogInformation("Going to set property {propertyName}", property); - var json = await context.FromBodyAsync>(new JsonSerializerOptions()) + var json = await context.FromBodyAsync(new JsonSerializerOptions()) .ConfigureAwait(false); - var result = thing.ThingContext.Properties.SetProperty(property, json[property]); + var result = thing.ThingContext.Properties.SetProperty(property, json.GetProperty(property)); if (result == SetPropertyResult.NotFound) { diff --git a/src/Mozilla.IoT.WebThing/EventCollection.cs b/src/Mozilla.IoT.WebThing/EventCollection.cs index 006d0f0..e860c5b 100644 --- a/src/Mozilla.IoT.WebThing/EventCollection.cs +++ b/src/Mozilla.IoT.WebThing/EventCollection.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Concurrent; +using System.Threading.Tasks; namespace Mozilla.IoT.WebThing { @@ -8,6 +10,8 @@ public class EventCollection private readonly object _locker = new object(); private readonly int _size; + public EventHandler Add; + public EventCollection(int size) { _size = size; @@ -28,6 +32,9 @@ public void Enqueue(Event @event) } _events.Enqueue(@event); + + var add = Add; + add?.Invoke(this, @event); } public void Dequeue() diff --git a/src/Mozilla.IoT.WebThing/Extensions/IEndpointRouteBuilderExtensions.cs b/src/Mozilla.IoT.WebThing/Extensions/IEndpointRouteBuilderExtensions.cs index cda3812..8c6b2ec 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/IEndpointRouteBuilderExtensions.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/IEndpointRouteBuilderExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Mozilla.IoT.WebThing; using Mozilla.IoT.WebThing.Endpoints; +using Mozilla.IoT.WebThing.WebSockets; namespace Microsoft.AspNetCore.Routing { @@ -17,7 +18,8 @@ public static void MapThings(this IEndpointRouteBuilder endpoint) } endpoint.MapGet("/things", GetAllThings.InvokeAsync); - endpoint.MapGet("/things/{name}", GetThing.InvokeAsync); + endpoint.MapGet("/things/{name}", context => context.WebSockets.IsWebSocketRequest + ? WebSocket.InvokeAsync(context) : GetThing.InvokeAsync(context)); endpoint.MapGet("/things/{name}/properties", GetProperties.InvokeAsync); endpoint.MapGet("/things/{name}/properties/{property}", GetProperty.InvokeAsync); endpoint.MapPut("/things/{name}/properties/{property}", PutProperty.InvokeAsync); diff --git a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs index b937fca..0f2c9f5 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs @@ -1,5 +1,10 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection.Extensions; using Mozilla.IoT.WebThing.Extensions; +using Mozilla.IoT.WebThing.WebSockets; namespace Microsoft.Extensions.DependencyInjection { @@ -16,6 +21,30 @@ public static IThingCollectionBuilder AddThings(this IServiceCollection service, options?.Invoke(thingOption); service.AddSingleton(thingOption); + + service.TryAddSingleton(provider => + { + var opt = provider.GetRequiredService(); + return new JsonSerializerOptions + { + PropertyNamingPolicy = opt.PropertyNamingPolicy, + PropertyNameCaseInsensitive = opt.IgnoreCase + }; + }); + + service.AddSingleton(); + service.AddSingleton(); + + service.AddSingleton(provider => + { + var opt = provider.GetRequiredService(); + var actions = provider.GetRequiredService>(); + + return actions.ToDictionary( + x => x.Action, + x => x, + opt.IgnoreCase ? StringComparer.InvariantCultureIgnoreCase : null); + }); var builder = new ThingCollectionBuilder(service); return builder; diff --git a/src/Mozilla.IoT.WebThing/Mozilla.IoT.WebThing.csproj b/src/Mozilla.IoT.WebThing/Mozilla.IoT.WebThing.csproj index 71fe718..dffa79f 100644 --- a/src/Mozilla.IoT.WebThing/Mozilla.IoT.WebThing.csproj +++ b/src/Mozilla.IoT.WebThing/Mozilla.IoT.WebThing.csproj @@ -14,6 +14,6 @@ - + diff --git a/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs b/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs new file mode 100644 index 0000000..b01a526 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs @@ -0,0 +1,43 @@ +using System; +using System.Net.WebSockets; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Mozilla.IoT.WebThing.WebSockets +{ + public class AddEventSubscription : IWebSocketAction + { + public string Action => "addEventSubscription"; + + public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, JsonElement data, JsonSerializerOptions options, + IServiceProvider provider, CancellationToken cancellationToken) + { + foreach (var (@event, collection) in thing.ThingContext.Events) + { + if (data.TryGetProperty(@event, out var value)) + { + collection.Add += (_, eventData) => + { + var sent = JsonSerializer.SerializeToUtf8Bytes(new NotifyEvent(eventData), options); + socket.SendAsync(sent, WebSocketMessageType.Text, true, cancellationToken) + .ConfigureAwait(false); + }; + } + } + + return Task.CompletedTask; + } + } + + public class NotifyEvent + { + public NotifyEvent(object data) + { + Data = data; + } + + public string MessageType => "event"; + public object Data { get; } + } +} diff --git a/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs b/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs new file mode 100644 index 0000000..1a3f4d0 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Mozilla.IoT.WebThing.WebSockets +{ + public interface IWebSocketAction + { + string Action { get; } + + Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, JsonElement data, + JsonSerializerOptions options, IServiceProvider provider, CancellationToken cancellationToken); + } +} diff --git a/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs b/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs new file mode 100644 index 0000000..f44f0c4 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs @@ -0,0 +1,58 @@ +using System; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Mozilla.IoT.WebThing.Actions; + +namespace Mozilla.IoT.WebThing.WebSockets +{ + public class RequestAction : IWebSocketAction + { + private static readonly ArraySegment s_errorMessage = new ArraySegment(Encoding.UTF8.GetBytes(@"{""messageType"": ""error"",""data"": {""status"": ""400 Bad Request"",""message"": ""Invalid action request""}}")); + + private readonly ILogger _logger; + + public RequestAction(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Action => "requestAction"; + + public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, JsonElement data, JsonSerializerOptions options, + IServiceProvider provider, CancellationToken cancellationToken) + { + foreach (var (actionName, actionContext) in thing.ThingContext.Actions) + { + if(!data.TryGetProperty(actionName, out var json)) + { + continue; + } + + _logger.LogTrace("{actionName} Action found. [Name: {thingName}]", actionName, thing.Name); + var actionInfo = (ActionInfo)JsonSerializer.Deserialize(json.GetBytesFromBase64(), actionContext.ActionType, options); + + if (!actionInfo.IsValid()) + { + _logger.LogInformation("{actionName} Action has invalid parameters. [Name: {thingName}]", actionName, thing.Name); + socket.SendAsync(s_errorMessage, WebSocketMessageType.Text, true, cancellationToken) + .ConfigureAwait(false); + continue; + } + + _logger.LogInformation("Going to execute {actionName} action. [Name: {thingName}]", actionName, thing.Name); + + actionInfo.ExecuteAsync(thing, provider) + .ConfigureAwait(false); + + thing.ThingContext.Actions[actionInfo.GetActionName()].Actions.Add(actionInfo.Id, actionInfo); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs new file mode 100644 index 0000000..d9ffc92 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs @@ -0,0 +1,26 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Mozilla.IoT.WebThing.WebSockets +{ + public class SetThingProperty : IWebSocketAction + { + private readonly ILogger _logger; + + public SetThingProperty(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Action => "setProperty"; + + public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, JsonElement data, JsonSerializerOptions options, + IServiceProvider provider, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs new file mode 100644 index 0000000..24f3b26 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs @@ -0,0 +1,166 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Mozilla.IoT.WebThing.Extensions; + +namespace Mozilla.IoT.WebThing.WebSockets +{ + internal class WebSocket + { + private static readonly ArrayPool s_pool = ArrayPool.Create(); + private static readonly ArraySegment s_error = new ArraySegment( + Encoding.UTF8.GetBytes( + @"{""messageType"": ""error"", ""data"": {""status"": ""400 Bad Request"",""message"": ""Invalid message""}}")); + + + + public static async Task InvokeAsync(HttpContext context) + { + var service = context.RequestServices; + var cancellation = context.RequestAborted; + + var logger = service.GetRequiredService>(); + + var things = service.GetRequiredService>(); + var option = service.GetRequiredService(); + + var name = context.GetRouteData("name"); + + var thing = option.IgnoreCase switch + { + true => things.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)), + _ => things.FirstOrDefault(x => x.Name == name) + }; + + if (thing == null) + { + logger.LogInformation("Thing not found. [Name: {name}]", name); + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + return; + } + + logger.LogInformation("Going to accept new Web Socket connection for {thing} Thing", name); + var socket = await context.WebSockets.AcceptWebSocketAsync() + .ConfigureAwait(false); + + + var id = Guid.NewGuid(); + thing.ThingContext.Sockets.TryAdd(id, socket); + + byte[] buffer = null; + + var actions = service.GetRequiredService>(); + + var jsonOptions = service.GetRequiredService(); + + var webSocketOption = service.GetService>(); + + try + { + BindActions(thing, socket, jsonOptions, cancellation); + + while (!socket.CloseStatus.HasValue && !cancellation.IsCancellationRequested) + { + if (buffer != null) + { + s_pool.Return(buffer, true); + } + + buffer = s_pool.Rent(webSocketOption.Value.ReceiveBufferSize); + + await socket + .ReceiveAsync(new ArraySegment(buffer), cancellation) + .ConfigureAwait(false); + + var json = JsonSerializer.Deserialize>(buffer, jsonOptions); + + if (!json.ContainsKey("messageType")) + { + logger.LogInformation("Web Socket request without messageType"); + await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + continue; + } + + if (!json.ContainsKey("data")) + { + logger.LogInformation("Web Socket request without data. [Message Type: {messageType}]", json["messageType"].GetString()); + await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + continue; + } + + if (!actions.TryGetValue(json["messageType"].GetString(), out var action)) + { + logger.LogInformation("Invalid Message Type: {messageType}", json["messageType"].GetString()); + await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + continue; + } + + try + { + using var scope = service.CreateScope(); + await action.ExecuteAsync(socket, thing, json["data"], jsonOptions, scope.ServiceProvider, cancellation) + .ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogError(e, "Error to execute Web Socket Action: {action}", json["messageType"].GetString()); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error to execute WebSocket, going to close connection"); + + await socket.CloseAsync(WebSocketCloseStatus.InternalServerError, ex.ToString(), + CancellationToken.None) + .ConfigureAwait(false); + } + + thing.ThingContext.Sockets.TryRemove(id, out _); + + if (buffer != null) + { + s_pool.Return(buffer, true); + } + } + + private static void BindActions(Thing thing, System.Net.WebSockets.WebSocket socket, JsonSerializerOptions jsonOptions, + CancellationToken cancellation) + { + foreach (var (actionName, actionContext) in thing.ThingContext.Actions) + { + + actionContext.Actions.Added += (_, actionAdded) => + { + if (socket == null) + { + return; + } + + socket.SendAsync( + JsonSerializer.SerializeToUtf8Bytes(new Dictionary + { + [actionName] = actionAdded + }, jsonOptions), + WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + }; + } + } + } +} From f210774aed48af8c10113b61083f6feeee51dccd Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Thu, 13 Feb 2020 07:35:58 +0000 Subject: [PATCH 2/9] Add Property web socket --- src/Mozilla.IoT.WebThing/Context.cs | 2 +- src/Mozilla.IoT.WebThing/Thing.cs | 51 ++++++++++++++++++- .../WebSockets/AddEventSubscription.cs | 20 +++----- .../WebSockets/SetThingProperty.cs | 21 +++++++- .../WebSockets/WebSocket.cs | 15 +++++- .../WebSockets/WebSocketResponse.cs | 26 ++++++++++ 6 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 src/Mozilla.IoT.WebThing/WebSockets/WebSocketResponse.cs diff --git a/src/Mozilla.IoT.WebThing/Context.cs b/src/Mozilla.IoT.WebThing/Context.cs index 4a6e5f7..ec860cc 100644 --- a/src/Mozilla.IoT.WebThing/Context.cs +++ b/src/Mozilla.IoT.WebThing/Context.cs @@ -22,7 +22,7 @@ public Context(IThingConverter converter, public IThingConverter Converter { get; } public IProperties Properties { get; } - + public LinkedList PropertiesName { get; } public Dictionary Events { get; } public Dictionary Actions { get; } public ConcurrentDictionary Sockets { get; } = new ConcurrentDictionary(); diff --git a/src/Mozilla.IoT.WebThing/Thing.cs b/src/Mozilla.IoT.WebThing/Thing.cs index fc66903..235dbb3 100644 --- a/src/Mozilla.IoT.WebThing/Thing.cs +++ b/src/Mozilla.IoT.WebThing/Thing.cs @@ -1,4 +1,6 @@ using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; using Mozilla.IoT.WebThing.Attributes; using static Mozilla.IoT.WebThing.Const; @@ -7,7 +9,7 @@ namespace Mozilla.IoT.WebThing /// /// The thing /// - public abstract class Thing + public abstract class Thing : INotifyPropertyChanged, IEquatable { #region Properties @@ -45,5 +47,52 @@ public abstract class Thing public virtual string[]? Type { get; } = null; #endregion + + public bool Equals(Thing other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Context == other.Context + && Title == other.Title + && Description == other.Description; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((Thing) obj); + } + + public override int GetHashCode() + => HashCode.Combine(Context, Title, Description); + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs b/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs index b01a526..9da4dc2 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace Mozilla.IoT.WebThing.WebSockets { @@ -10,7 +11,8 @@ public class AddEventSubscription : IWebSocketAction { public string Action => "addEventSubscription"; - public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, JsonElement data, JsonSerializerOptions options, + public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, JsonElement data, + JsonSerializerOptions options, IServiceProvider provider, CancellationToken cancellationToken) { foreach (var (@event, collection) in thing.ThingContext.Events) @@ -19,25 +21,15 @@ public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, Js { collection.Add += (_, eventData) => { - var sent = JsonSerializer.SerializeToUtf8Bytes(new NotifyEvent(eventData), options); + var sent = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("event", eventData), + options); socket.SendAsync(sent, WebSocketMessageType.Text, true, cancellationToken) .ConfigureAwait(false); }; } } - - return Task.CompletedTask; - } - } - public class NotifyEvent - { - public NotifyEvent(object data) - { - Data = data; + return Task.CompletedTask; } - - public string MessageType => "event"; - public object Data { get; } } } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs index d9ffc92..00c9882 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs @@ -1,4 +1,5 @@ using System; +using System.Net.WebSockets; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -20,7 +21,25 @@ public SetThingProperty(ILogger logger) public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, JsonElement data, JsonSerializerOptions options, IServiceProvider provider, CancellationToken cancellationToken) { - throw new NotImplementedException(); + foreach (var propertyName in thing.ThingContext.PropertiesName) + { + if (!data.TryGetProperty(propertyName, out var property)) + { + continue; + } + + var result = thing.ThingContext.Properties.SetProperty(propertyName, data); + if (result == SetPropertyResult.InvalidValue) + { + _logger.LogInformation("Invalid property value"); + var response = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("error", new ErrorResponse("400 Bad Request", "Invalid Property")), options); + + socket.SendAsync(response, WebSocketMessageType.Text, true, cancellationToken) + .ConfigureAwait(false); + } + } + + return Task.CompletedTask; } } } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs index 24f3b26..8f69f37 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs @@ -70,7 +70,8 @@ public static async Task InvokeAsync(HttpContext context) try { BindActions(thing, socket, jsonOptions, cancellation); - + BindPropertyChange(thing, socket, jsonOptions, cancellation); + while (!socket.CloseStatus.HasValue && !cancellation.IsCancellationRequested) { if (buffer != null) @@ -162,5 +163,17 @@ private static void BindActions(Thing thing, System.Net.WebSockets.WebSocket soc }; } } + + private static void BindPropertyChange(Thing thing, System.Net.WebSockets.WebSocket socket, JsonSerializerOptions jsonOptions, + CancellationToken cancellation) + { + thing.PropertyChanged += (sender, args) => + { + socket.SendAsync(JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("setProperty", + thing.ThingContext.Properties.GetProperties(args.PropertyName)), jsonOptions), + WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + }; + } } } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/WebSocketResponse.cs b/src/Mozilla.IoT.WebThing/WebSockets/WebSocketResponse.cs new file mode 100644 index 0000000..b46d2a8 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/WebSocketResponse.cs @@ -0,0 +1,26 @@ +namespace Mozilla.IoT.WebThing.WebSockets +{ + public class WebSocketResponse + { + public WebSocketResponse(string messageType, object data) + { + MessageType = messageType; + Data = data; + } + + public string MessageType { get; } + public object Data { get; } + } + + public class ErrorResponse + { + public ErrorResponse(string status, string message) + { + Status = status; + Message = message; + } + + public string Status { get; } + public string Message { get; } + } +} From 41287d8886064faaeea2e8bbb2b85e6880a4a668 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Thu, 13 Feb 2020 18:27:17 +0000 Subject: [PATCH 3/9] Add Actions socket actions --- .../MultiThing/Things/ExampleDimmableLight.cs | 6 +- .../Things/FakeGpioHumiditySensor.cs | 6 +- sample/SampleThing/Things/LampThing.cs | 2 +- src/Mozilla.IoT.WebThing/ActionCollection.cs | 27 ++++++-- src/Mozilla.IoT.WebThing/ActionContext.cs | 2 - .../Actions/ActionInfo.cs | 10 +++ src/Mozilla.IoT.WebThing/Context.cs | 1 - .../Endpoints/GetAction.cs | 1 - .../Endpoints/GetActionById.cs | 1 - .../Endpoints/GetActions.cs | 1 - .../Endpoints/GetEvent.cs | 1 - .../Endpoints/GetProperty.cs | 1 - src/Mozilla.IoT.WebThing/EventCollection.cs | 9 ++- .../Extensions/IServiceExtensions.cs | 21 ++++-- src/Mozilla.IoT.WebThing/IProperties.cs | 1 + src/Mozilla.IoT.WebThing/Properties.cs | 2 + src/Mozilla.IoT.WebThing/Thing.cs | 4 +- .../WebSockets/AddEventSubscription.cs | 14 ++-- .../WebSockets/IWebSocketAction.cs | 1 - .../WebSockets/RequestAction.cs | 1 - .../WebSockets/SetThingProperty.cs | 4 +- .../WebSockets/ThingObserver.cs | 67 +++++++++++++++++++ .../WebSockets/WebSocket.cs | 65 +++++++++--------- .../Things/LampThing.cs | 6 +- 24 files changed, 171 insertions(+), 83 deletions(-) create mode 100644 src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs diff --git a/sample/MultiThing/Things/ExampleDimmableLight.cs b/sample/MultiThing/Things/ExampleDimmableLight.cs index f9e4549..9b84ee4 100644 --- a/sample/MultiThing/Things/ExampleDimmableLight.cs +++ b/sample/MultiThing/Things/ExampleDimmableLight.cs @@ -11,11 +11,11 @@ public class ExampleDimmableLight : Thing { public override string Name => "my-lamp-1234"; - public override string? Title => "My Lamp"; + public override string Title => "My Lamp"; - public override string[]? Type { get; } = new[] {"OnOffSwitch", "Light"}; + public override string[] Type { get; } = new[] {"OnOffSwitch", "Light"}; - public override string? Description => "A web connected lamp"; + public override string Description => "A web connected lamp"; private bool _on = true; diff --git a/sample/MultiThing/Things/FakeGpioHumiditySensor.cs b/sample/MultiThing/Things/FakeGpioHumiditySensor.cs index 646020a..52043c4 100644 --- a/sample/MultiThing/Things/FakeGpioHumiditySensor.cs +++ b/sample/MultiThing/Things/FakeGpioHumiditySensor.cs @@ -24,11 +24,11 @@ public FakeGpioHumiditySensor() } public override string Name => "my-humidity-sensor-1234"; - public override string? Title => "My Humidity Sensor"; + public override string Title => "My Humidity Sensor"; - public override string[]? Type { get; } = new[] {"MultiLevelSensor"}; + public override string[] Type { get; } = new[] {"MultiLevelSensor"}; - public override string? Description => "A web connected humidity sensor"; + public override string Description => "A web connected humidity sensor"; [ThingProperty(Type = new []{"LevelProperty"}, Title = "Humidity", Description = "The current humidity in %", diff --git a/sample/SampleThing/Things/LampThing.cs b/sample/SampleThing/Things/LampThing.cs index 3ea6cb8..6c26c66 100644 --- a/sample/SampleThing/Things/LampThing.cs +++ b/sample/SampleThing/Things/LampThing.cs @@ -24,7 +24,7 @@ public class LampThing : Thing [ThingEvent(Title = "Overheated", Unit = "degree celsius", Type = new [] {"OverheatedEvent"}, Description = "The lamp has exceeded its safe operating temperature")] - public event EventHandler Overheated; + public event EventHandler? Overheated; [ThingAction(Name = "fade", Title = "Fade", Type = new []{"FadeAction"}, diff --git a/src/Mozilla.IoT.WebThing/ActionCollection.cs b/src/Mozilla.IoT.WebThing/ActionCollection.cs index d861182..c656a04 100644 --- a/src/Mozilla.IoT.WebThing/ActionCollection.cs +++ b/src/Mozilla.IoT.WebThing/ActionCollection.cs @@ -10,7 +10,7 @@ public class ActionCollection : IEnumerable { private readonly ConcurrentDictionary _actions; - public event EventHandler Added; + public event EventHandler? Change; public ActionCollection() { @@ -21,15 +21,32 @@ public void Add(Guid id, ActionInfo actionInfo) { _actions.TryAdd(id, actionInfo); - var added = Added; - added?.Invoke(this, actionInfo); + actionInfo.StatusChanged += OnStatusChange; + + var change = Change; + change?.Invoke(this, actionInfo); } - public bool TryGetValue(Guid id, out ActionInfo action) + public bool TryGetValue(Guid id, out ActionInfo? action) => _actions.TryGetValue(id, out action); public bool TryRemove(Guid id, out ActionInfo action) - => _actions.TryRemove(id, out action); + { + var result =_actions.TryRemove(id, out action); + if (result && action != null) + { + + action.StatusChanged -= OnStatusChange; + } + + return result; + } + + private void OnStatusChange(object? sender, EventArgs args) + { + var change = Change; + change?.Invoke(this, (ActionInfo)sender); + } public IEnumerator GetEnumerator() => _actions.Values.GetEnumerator(); diff --git a/src/Mozilla.IoT.WebThing/ActionContext.cs b/src/Mozilla.IoT.WebThing/ActionContext.cs index a6c2969..730c1cc 100644 --- a/src/Mozilla.IoT.WebThing/ActionContext.cs +++ b/src/Mozilla.IoT.WebThing/ActionContext.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Concurrent; -using Mozilla.IoT.WebThing.Actions; namespace Mozilla.IoT.WebThing { diff --git a/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs b/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs index b58bb9b..1d95973 100644 --- a/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs +++ b/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs @@ -27,8 +27,13 @@ public async Task ExecuteAsync(Thing thing, IServiceProvider provider) { var logger = provider.GetRequiredService>(); logger.LogInformation("Going to execute {actionName}", ActionName); + + var status = StatusChanged; + Status = "executing"; + status?.Invoke(this, EventArgs.Empty); + try { await InternalExecuteAsync(thing, provider) @@ -42,7 +47,11 @@ await InternalExecuteAsync(thing, provider) } TimeCompleted = DateTime.UtcNow; + Status = "completed"; + + status?.Invoke(this, EventArgs.Empty); + } internal string GetActionName() => ActionName; @@ -50,5 +59,6 @@ await InternalExecuteAsync(thing, provider) public void Cancel() => Source.Cancel(); + public event EventHandler? StatusChanged; } } diff --git a/src/Mozilla.IoT.WebThing/Context.cs b/src/Mozilla.IoT.WebThing/Context.cs index ec860cc..a92e716 100644 --- a/src/Mozilla.IoT.WebThing/Context.cs +++ b/src/Mozilla.IoT.WebThing/Context.cs @@ -22,7 +22,6 @@ public Context(IThingConverter converter, public IThingConverter Converter { get; } public IProperties Properties { get; } - public LinkedList PropertiesName { get; } public Dictionary Events { get; } public Dictionary Actions { get; } public ConcurrentDictionary Sockets { get; } = new ConcurrentDictionary(); diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs index b3a07a5..b7259cb 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetAction.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs index ef4f3f1..7fe8b9e 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetActionById.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs index 19464df..59153ca 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetActions.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs index e34ab61..bd1b43b 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs @@ -5,7 +5,6 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Converts; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs index 87e24e9..d71214f 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Converts; -using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Endpoints { diff --git a/src/Mozilla.IoT.WebThing/EventCollection.cs b/src/Mozilla.IoT.WebThing/EventCollection.cs index e860c5b..5f71fde 100644 --- a/src/Mozilla.IoT.WebThing/EventCollection.cs +++ b/src/Mozilla.IoT.WebThing/EventCollection.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Concurrent; -using System.Threading.Tasks; namespace Mozilla.IoT.WebThing { - public class EventCollection + public class EventCollection { private readonly ConcurrentQueue _events; private readonly object _locker = new object(); private readonly int _size; - public EventHandler Add; - + public event EventHandler? Added; + public EventCollection(int size) { _size = size; @@ -33,7 +32,7 @@ public void Enqueue(Event @event) _events.Enqueue(@event); - var add = Add; + var add = Added; add?.Invoke(this, @event); } diff --git a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs index 0f2c9f5..86d636b 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs @@ -10,7 +10,8 @@ namespace Microsoft.Extensions.DependencyInjection { public static class IServiceExtensions { - public static IThingCollectionBuilder AddThings(this IServiceCollection service, Action? options = null) + public static IThingCollectionBuilder AddThings(this IServiceCollection service, + Action? options = null) { if (service == null) { @@ -27,14 +28,16 @@ public static IThingCollectionBuilder AddThings(this IServiceCollection service, var opt = provider.GetRequiredService(); return new JsonSerializerOptions { - PropertyNamingPolicy = opt.PropertyNamingPolicy, - PropertyNameCaseInsensitive = opt.IgnoreCase + PropertyNamingPolicy = opt.PropertyNamingPolicy, PropertyNameCaseInsensitive = opt.IgnoreCase }; }); - + service.AddSingleton(); service.AddSingleton(); - + + service.AddScoped(); + service.AddScoped(provider => provider.GetRequiredService().Observer); + service.AddSingleton(provider => { var opt = provider.GetRequiredService(); @@ -45,9 +48,15 @@ public static IThingCollectionBuilder AddThings(this IServiceCollection service, x => x, opt.IgnoreCase ? StringComparer.InvariantCultureIgnoreCase : null); }); - + var builder = new ThingCollectionBuilder(service); return builder; } } + + public class ThingObserverResolver + { + public ThingObserver Observer { get; set; } = default!; + } + } diff --git a/src/Mozilla.IoT.WebThing/IProperties.cs b/src/Mozilla.IoT.WebThing/IProperties.cs index aff3b61..2d25b57 100644 --- a/src/Mozilla.IoT.WebThing/IProperties.cs +++ b/src/Mozilla.IoT.WebThing/IProperties.cs @@ -4,6 +4,7 @@ namespace Mozilla.IoT.WebThing { public interface IProperties { + IEnumerable PropertiesNames { get; } Dictionary? GetProperties(string? propertyName = null); SetPropertyResult SetProperty(string propertyName, object value); diff --git a/src/Mozilla.IoT.WebThing/Properties.cs b/src/Mozilla.IoT.WebThing/Properties.cs index 1706d20..c681a94 100644 --- a/src/Mozilla.IoT.WebThing/Properties.cs +++ b/src/Mozilla.IoT.WebThing/Properties.cs @@ -16,6 +16,8 @@ public Properties(Thing thing, _properties = properties ?? throw new ArgumentNullException(nameof(properties)); } + public IEnumerable PropertiesNames => _properties.Keys; + public Dictionary? GetProperties(string? propertyName = null) { if (propertyName == null) diff --git a/src/Mozilla.IoT.WebThing/Thing.cs b/src/Mozilla.IoT.WebThing/Thing.cs index 235dbb3..750d7a2 100644 --- a/src/Mozilla.IoT.WebThing/Thing.cs +++ b/src/Mozilla.IoT.WebThing/Thing.cs @@ -65,7 +65,7 @@ public bool Equals(Thing other) && Description == other.Description; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { @@ -90,7 +90,7 @@ public override int GetHashCode() public event PropertyChangedEventHandler PropertyChanged; - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs b/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs index 9da4dc2..65b0ece 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs @@ -1,9 +1,8 @@ using System; -using System.Net.WebSockets; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; namespace Mozilla.IoT.WebThing.WebSockets { @@ -15,17 +14,12 @@ public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, Js JsonSerializerOptions options, IServiceProvider provider, CancellationToken cancellationToken) { + var observer = provider.GetRequiredService(); foreach (var (@event, collection) in thing.ThingContext.Events) { - if (data.TryGetProperty(@event, out var value)) + if (data.TryGetProperty(@event, out _)) { - collection.Add += (_, eventData) => - { - var sent = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("event", eventData), - options); - socket.SendAsync(sent, WebSocketMessageType.Text, true, cancellationToken) - .ConfigureAwait(false); - }; + collection.Added += observer.OnEvenAdded; } } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs b/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs index 1a3f4d0..5d8745f 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; diff --git a/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs b/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs index f44f0c4..ada453b 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs @@ -1,5 +1,4 @@ using System; -using System.Net; using System.Net.WebSockets; using System.Text; using System.Text.Json; diff --git a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs index 00c9882..0aabdf4 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs @@ -21,14 +21,14 @@ public SetThingProperty(ILogger logger) public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, JsonElement data, JsonSerializerOptions options, IServiceProvider provider, CancellationToken cancellationToken) { - foreach (var propertyName in thing.ThingContext.PropertiesName) + foreach (var propertyName in thing.ThingContext.Properties.PropertiesNames) { if (!data.TryGetProperty(propertyName, out var property)) { continue; } - var result = thing.ThingContext.Properties.SetProperty(propertyName, data); + var result = thing.ThingContext.Properties.SetProperty(propertyName, property); if (result == SetPropertyResult.InvalidValue) { _logger.LogInformation("Invalid property value"); diff --git a/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs new file mode 100644 index 0000000..b2b7034 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Net.WebSockets; +using System.Text.Json; +using System.Threading; +using Microsoft.Extensions.Logging; +using Mozilla.IoT.WebThing.Actions; + +namespace Mozilla.IoT.WebThing.WebSockets +{ + public class ThingObserver + { + private readonly ILogger _logger; + private readonly Thing _thing; + private readonly JsonSerializerOptions _options; + private readonly System.Net.WebSockets.WebSocket _socket; + private readonly CancellationToken _cancellation; + + public ThingObserver(ILogger logger, + JsonSerializerOptions options, + System.Net.WebSockets.WebSocket socket, + CancellationToken cancellation, + Thing thing) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _socket = socket ?? throw new ArgumentNullException(nameof(socket)); + _cancellation = cancellation; + _thing = thing ?? throw new ArgumentNullException(nameof(thing)); + } + + public HashSet EventsBind { get; } = new HashSet(); + + public async void OnEvenAdded(object sender, Event @event) + { + _logger.LogInformation("Event add received, going to notify Web Socket"); + var sent = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("event", @event), + _options); + + await _socket.SendAsync(sent, WebSocketMessageType.Text, true, _cancellation) + .ConfigureAwait(false); + } + + public async void OnPropertyChanged(object sender, PropertyChangedEventArgs property) + { + _logger.LogInformation("Event add received, going to notify Web Socket"); + var sent = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("event", + _thing.ThingContext.Properties.GetProperties(property.PropertyName)), + _options); + + await _socket.SendAsync(sent, WebSocketMessageType.Text, true, _cancellation) + .ConfigureAwait(false); + } + + public async void OnActionChange(object sender, ActionInfo action) + { + await _socket.SendAsync( + JsonSerializer.SerializeToUtf8Bytes(new Dictionary + { + [action.GetActionName()] = action + }, _options), + WebSocketMessageType.Text, true, _cancellation) + .ConfigureAwait(false); + } + } +} diff --git a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs index 8f69f37..4a2ce88 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs @@ -23,9 +23,6 @@ internal class WebSocket private static readonly ArraySegment s_error = new ArraySegment( Encoding.UTF8.GetBytes( @"{""messageType"": ""error"", ""data"": {""status"": ""400 Bad Request"",""message"": ""Invalid message""}}")); - - - public static async Task InvokeAsync(HttpContext context) { var service = context.RequestServices; @@ -59,7 +56,7 @@ public static async Task InvokeAsync(HttpContext context) var id = Guid.NewGuid(); thing.ThingContext.Sockets.TryAdd(id, socket); - byte[] buffer = null; + byte[]? buffer = null; var actions = service.GetRequiredService>(); @@ -67,10 +64,13 @@ public static async Task InvokeAsync(HttpContext context) var webSocketOption = service.GetService>(); + var observer = new ThingObserver(service.GetRequiredService>(), + jsonOptions, socket, cancellation, thing); + try { - BindActions(thing, socket, jsonOptions, cancellation); - BindPropertyChange(thing, socket, jsonOptions, cancellation); + BindActions(thing, observer); + BindPropertyChanged(thing, observer); while (!socket.CloseStatus.HasValue && !cancellation.IsCancellationRequested) { @@ -114,6 +114,7 @@ await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) try { using var scope = service.CreateScope(); + scope.ServiceProvider.GetRequiredService().Observer = observer; await action.ExecuteAsync(socket, thing, json["data"], jsonOptions, scope.ServiceProvider, cancellation) .ConfigureAwait(false); } @@ -138,42 +139,40 @@ await socket.CloseAsync(WebSocketCloseStatus.InternalServerError, ex.ToString(), { s_pool.Return(buffer, true); } + + UnbindActions(thing, observer); + UnbindPropertyChanged(thing, observer); + UnbindEvent(thing, observer); } - private static void BindActions(Thing thing, System.Net.WebSockets.WebSocket socket, JsonSerializerOptions jsonOptions, - CancellationToken cancellation) + private static void BindActions(Thing thing, ThingObserver observer) { - foreach (var (actionName, actionContext) in thing.ThingContext.Actions) + foreach (var (_, actionContext) in thing.ThingContext.Actions) { - - actionContext.Actions.Added += (_, actionAdded) => - { - if (socket == null) - { - return; - } - - socket.SendAsync( - JsonSerializer.SerializeToUtf8Bytes(new Dictionary - { - [actionName] = actionAdded - }, jsonOptions), - WebSocketMessageType.Text, true, cancellation) - .ConfigureAwait(false); - }; + actionContext.Actions.Change += observer.OnActionChange; } } + + private static void BindPropertyChanged(Thing thing, ThingObserver observer) + => thing.PropertyChanged += observer.OnPropertyChanged; - private static void BindPropertyChange(Thing thing, System.Net.WebSockets.WebSocket socket, JsonSerializerOptions jsonOptions, - CancellationToken cancellation) + private static void UnbindActions(Thing thing, ThingObserver observer) { - thing.PropertyChanged += (sender, args) => + foreach (var (_, actionContext) in thing.ThingContext.Actions) { - socket.SendAsync(JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("setProperty", - thing.ThingContext.Properties.GetProperties(args.PropertyName)), jsonOptions), - WebSocketMessageType.Text, true, cancellation) - .ConfigureAwait(false); - }; + actionContext.Actions.Change -= observer.OnActionChange; + } + } + + private static void UnbindPropertyChanged(Thing thing, ThingObserver observer) + => thing.PropertyChanged -= observer.OnPropertyChanged; + + private static void UnbindEvent(Thing thing, ThingObserver observer) + { + foreach (var @event in observer.EventsBind) + { + thing.ThingContext.Events[@event].Added -= observer.OnEvenAdded; + } } } } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs index c117a8e..8c5b7d1 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs @@ -31,9 +31,9 @@ public LampThing() }); } public override string Name => "Lamp"; - public override string? Title => "My Lamp"; - public override string? Description => "A web connected lamp"; - public override string[]? Type { get; } = new[] { "Light", "OnOffSwitch" }; + public override string Title => "My Lamp"; + public override string Description => "A web connected lamp"; + public override string[] Type { get; } = new[] { "Light", "OnOffSwitch" }; [ThingProperty(Type = new []{ "OnOffProperty" }, Title = "On/Off", Description = "Whether the lamp is turned on")] public bool On { get; set; } From 495fb7244b0268f59c82efa46e000c7da4f5bd60 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Thu, 13 Feb 2020 18:45:42 +0000 Subject: [PATCH 4/9] Improve test --- .../Http/Events.cs | 3 +-- .../Http/Properties.cs | 15 ++++-------- .../Http/Thing.cs | 9 +++---- .../Program.cs | 24 +++++++++++++++++++ 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Events.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Events.cs index e9754f6..ee9ca98 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Events.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Events.cs @@ -108,8 +108,7 @@ public async Task GetEvent() [Fact] public async Task GetInvalidEvent() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var response = await client.GetAsync("/things/Lamp/events/aaaaa"); diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs index b8f4df3..6cc08f8 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs @@ -23,8 +23,7 @@ public Properties() [Fact] public async Task GetAll() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var response = await client.GetAsync("/things/Lamp/properties"); @@ -51,8 +50,7 @@ public async Task GetAll() [InlineData("brightness", 0)] public async Task Get(string property, object value) { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var response = await client.GetAsync($"/things/Lamp/properties/{property}"); @@ -72,8 +70,7 @@ public async Task Get(string property, object value) [Fact] public async Task GetInvalid() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var response = await client.GetAsync($"/things/Lamp/properties/{_fixture.Create()}"); @@ -127,8 +124,7 @@ public async Task Put(string property, object value) [InlineData("brightness", 101, 0)] public async Task PutInvalidValue(string property, object value, object defaulValue) { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var response = await client.PutAsync($"/things/Lamp/properties/{property}", new StringContent($@"{{ ""{property}"": {value.ToString().ToLower()} }}")); @@ -154,8 +150,7 @@ public async Task PutInvalidValue(string property, object value, object defaulVa [Fact] public async Task PutInvalidProperty() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var property = _fixture.Create(); var response = await client.PutAsync($"/things/Lamp/properties/{property}", diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index 00eafa4..470b2b3 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -14,8 +14,7 @@ public class Thing [Fact] public async Task GetAll() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var response = await client.GetAsync("/things"); @@ -152,8 +151,7 @@ public async Task GetAll() [Fact] public async Task Get() { - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var response = await client.GetAsync("/things/Lamp"); @@ -289,8 +287,7 @@ public async Task Get() public async Task GetInvalid() { var fixture = new Fixture(); - var host = await Program.CreateHostBuilder(null) - .StartAsync(); + var host = await Program.GetHost(); var client = host.GetTestServer().CreateClient(); var response = await client.GetAsync($"/things/{fixture.Create()}"); diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs index c8f8188..7172673 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Program.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; @@ -14,5 +15,28 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .UseTestServer() .UseStartup(); }); + + private static IHost s_defaultHost; + + public static ValueTask GetHost() + { + if (s_defaultHost != null) + { + return new ValueTask(s_defaultHost); + } + + return new ValueTask(CreateHostBuilderAndStartAsync(null)); + } + + private static async Task CreateHostBuilderAndStartAsync(string[] args) + { + return s_defaultHost = await Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .UseTestServer() + .UseStartup(); + }).StartAsync(); + } } } From 2740f134a514f0cbd8d8cb3e71749ae1d53144ec Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Thu, 13 Feb 2020 21:08:15 +0000 Subject: [PATCH 5/9] fixes set and get property in websocket --- .../Endpoints/GetEvent.cs | 2 +- .../Endpoints/GetEvents.cs | 2 +- .../Endpoints/GetProperties.cs | 2 +- .../Endpoints/GetProperty.cs | 2 +- .../Endpoints/PutProperty.cs | 6 +- .../Extensions/IServiceExtensions.cs | 4 +- .../Properties/PropertiesIntercept.cs | 4 +- src/Mozilla.IoT.WebThing/Thing.cs | 2 +- .../WebSockets/SetThingProperty.cs | 2 +- .../WebSockets/ThingObserver.cs | 3 +- .../WebSockets/WebSocket.cs | 26 +++--- .../Startup.cs | 5 ++ .../Things/LampThing.cs | 29 +++++-- .../WebScokets/Property.cs | 79 +++++++++++++++++++ .../WebScokets/WebSocketBody.cs | 8 ++ 15 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs create mode 100644 test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/WebSocketBody.cs diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs index bd1b43b..9ea4624 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetEvent.cs @@ -51,7 +51,7 @@ public static Task InvokeAsync(HttpContext context) context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; - return JsonSerializer.SerializeAsync(context.Response.Body, result, ThingConverter.Options); + return JsonSerializer.SerializeAsync(context.Response.Body, result, service.GetRequiredService()); } } } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs index 2679186..bc2c289 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetEvents.cs @@ -47,7 +47,7 @@ public static Task InvokeAsync(HttpContext context) context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; - return JsonSerializer.SerializeAsync(context.Response.Body, result, ThingConverter.Options); + return JsonSerializer.SerializeAsync(context.Response.Body, result, service.GetRequiredService()); } } } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs index c68ee8b..4e0386f 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetProperties.cs @@ -37,7 +37,7 @@ public static Task InvokeAsync(HttpContext context) context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; - return JsonSerializer.SerializeAsync(context.Response.Body, properties, ThingConverter.Options); + return JsonSerializer.SerializeAsync(context.Response.Body, properties, service.GetRequiredService()); } } } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs index d71214f..50ba07f 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/GetProperty.cs @@ -46,7 +46,7 @@ public static Task InvokeAsync(HttpContext context) context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentType = Const.ContentType; - return JsonSerializer.SerializeAsync(context.Response.Body, properties, ThingConverter.Options); + return JsonSerializer.SerializeAsync(context.Response.Body, properties, service.GetRequiredService()); } } } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs index 7ceecb4..1215602 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs @@ -33,8 +33,10 @@ public static async Task InvokeAsync(HttpContext context) var property = context.GetRouteData("property"); logger.LogInformation("Going to set property {propertyName}", property); + + var jsonOptions = service.GetRequiredService(); - var json = await context.FromBodyAsync(new JsonSerializerOptions()) + var json = await context.FromBodyAsync(jsonOptions) .ConfigureAwait(false); var result = thing.ThingContext.Properties.SetProperty(property, json.GetProperty(property)); @@ -53,7 +55,7 @@ public static async Task InvokeAsync(HttpContext context) return; } - await context.WriteBodyAsync(HttpStatusCode.OK, thing.ThingContext.Properties.GetProperties(property), ThingConverter.Options) + await context.WriteBodyAsync(HttpStatusCode.OK, thing.ThingContext.Properties.GetProperties(property), jsonOptions) .ConfigureAwait(false); } } diff --git a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs index 86d636b..0ab08e2 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs @@ -28,12 +28,14 @@ public static IThingCollectionBuilder AddThings(this IServiceCollection service, var opt = provider.GetRequiredService(); return new JsonSerializerOptions { - PropertyNamingPolicy = opt.PropertyNamingPolicy, PropertyNameCaseInsensitive = opt.IgnoreCase + PropertyNamingPolicy = opt.PropertyNamingPolicy, + DictionaryKeyPolicy = opt.PropertyNamingPolicy }; }); service.AddSingleton(); service.AddSingleton(); + service.AddSingleton(); service.AddScoped(); service.AddScoped(provider => provider.GetRequiredService().Observer); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs index a9070ba..6e3fa62 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs @@ -12,9 +12,11 @@ namespace Mozilla.IoT.WebThing.Factories.Generator.Properties internal class PropertiesIntercept : IPropertyIntercept { public Dictionary Properties { get; } + private readonly ThingOption _option; public PropertiesIntercept(ThingOption option) { + _option = option ?? throw new ArgumentNullException(nameof(option)); Properties = option.IgnoreCase ? new Dictionary(StringComparer.InvariantCultureIgnoreCase) : new Dictionary(); } @@ -27,7 +29,7 @@ public void Before(Thing thing) public void Intercept(Thing thing, PropertyInfo propertyInfo, ThingPropertyAttribute? thingPropertyAttribute) { var propertyName = thingPropertyAttribute?.Name ?? propertyInfo.Name; - Properties.Add(propertyName, new Property(GetGetMethod(propertyInfo), + Properties.Add(_option.PropertyNamingPolicy.ConvertName(propertyName), new Property(GetGetMethod(propertyInfo), GetSetMethod(propertyInfo), CreateValidator(propertyInfo, thingPropertyAttribute), CreateMapper(propertyInfo.PropertyType))); diff --git a/src/Mozilla.IoT.WebThing/Thing.cs b/src/Mozilla.IoT.WebThing/Thing.cs index 750d7a2..b434604 100644 --- a/src/Mozilla.IoT.WebThing/Thing.cs +++ b/src/Mozilla.IoT.WebThing/Thing.cs @@ -90,7 +90,7 @@ public override int GetHashCode() public event PropertyChangedEventHandler PropertyChanged; - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + protected virtual void OnPropertyChanged([CallerMemberName]string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs index 0aabdf4..bf87071 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs @@ -23,7 +23,7 @@ public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, Js { foreach (var propertyName in thing.ThingContext.Properties.PropertiesNames) { - if (!data.TryGetProperty(propertyName, out var property)) + if (!data.TryGetProperty(options.PropertyNamingPolicy.ConvertName(propertyName), out var property)) { continue; } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs index b2b7034..1ff85e4 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs @@ -45,7 +45,7 @@ await _socket.SendAsync(sent, WebSocketMessageType.Text, true, _cancellation) public async void OnPropertyChanged(object sender, PropertyChangedEventArgs property) { _logger.LogInformation("Event add received, going to notify Web Socket"); - var sent = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("event", + var sent = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("propertyStatus", _thing.ThingContext.Properties.GetProperties(property.PropertyName)), _options); @@ -58,6 +58,7 @@ public async void OnActionChange(object sender, ActionInfo action) await _socket.SendAsync( JsonSerializer.SerializeToUtf8Bytes(new Dictionary { + ["messageType"] = "actionStatus", [action.GetActionName()] = action }, _options), WebSocketMessageType.Text, true, _cancellation) diff --git a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs index 4a2ce88..691f6ea 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs @@ -62,7 +62,7 @@ public static async Task InvokeAsync(HttpContext context) var jsonOptions = service.GetRequiredService(); - var webSocketOption = service.GetService>(); + var webSocketOption = service.GetRequiredService>().Value; var observer = new ThingObserver(service.GetRequiredService>(), jsonOptions, socket, cancellation, thing); @@ -79,15 +79,15 @@ public static async Task InvokeAsync(HttpContext context) s_pool.Return(buffer, true); } - buffer = s_pool.Rent(webSocketOption.Value.ReceiveBufferSize); - - await socket - .ReceiveAsync(new ArraySegment(buffer), cancellation) + buffer = s_pool.Rent(webSocketOption.ReceiveBufferSize); + var segment = new ArraySegment(buffer); + var received = await socket + .ReceiveAsync(segment, cancellation) .ConfigureAwait(false); - var json = JsonSerializer.Deserialize>(buffer, jsonOptions); + var json = JsonSerializer.Deserialize(segment.Slice(0, received.Count), jsonOptions); - if (!json.ContainsKey("messageType")) + if (!json.TryGetProperty("messageType", out var messageType)) { logger.LogInformation("Web Socket request without messageType"); await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) @@ -95,17 +95,17 @@ await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) continue; } - if (!json.ContainsKey("data")) + if (!json.TryGetProperty("data", out var data)) { - logger.LogInformation("Web Socket request without data. [Message Type: {messageType}]", json["messageType"].GetString()); + logger.LogInformation("Web Socket request without data. [Message Type: {messageType}]", messageType.GetString()); await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) .ConfigureAwait(false); continue; } - if (!actions.TryGetValue(json["messageType"].GetString(), out var action)) + if (!actions.TryGetValue(messageType.GetString(), out var action)) { - logger.LogInformation("Invalid Message Type: {messageType}", json["messageType"].GetString()); + logger.LogInformation("Invalid Message Type: {messageType}", messageType.GetString()); await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) .ConfigureAwait(false); continue; @@ -115,12 +115,12 @@ await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) { using var scope = service.CreateScope(); scope.ServiceProvider.GetRequiredService().Observer = observer; - await action.ExecuteAsync(socket, thing, json["data"], jsonOptions, scope.ServiceProvider, cancellation) + await action.ExecuteAsync(socket, thing, data, jsonOptions, scope.ServiceProvider, cancellation) .ConfigureAwait(false); } catch (Exception e) { - logger.LogError(e, "Error to execute Web Socket Action: {action}", json["messageType"].GetString()); + logger.LogError(e, "Error to execute Web Socket Action: {action}", messageType.GetString()); } } } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs index 3f956e8..d115183 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Startup.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.WebSockets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Mozilla.IoT.WebThing.AcceptanceTest.Things; @@ -15,6 +16,8 @@ public void ConfigureServices(IServiceCollection services) { services.AddThings() .AddThing(); + + services.AddWebSockets(o => { }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -27,6 +30,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); + app.UseWebSockets(); + app.UseEndpoints(endpoints => { endpoints.MapThings(); diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs index 8c5b7d1..e2c1104 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs @@ -35,12 +35,31 @@ public LampThing() public override string Description => "A web connected lamp"; public override string[] Type { get; } = new[] { "Light", "OnOffSwitch" }; - [ThingProperty(Type = new []{ "OnOffProperty" }, Title = "On/Off", Description = "Whether the lamp is turned on")] - public bool On { get; set; } - - [ThingProperty(Type = new []{ "BrightnessProperty" },Title = "Brightness", + private bool _on; + [ThingProperty(Type = new[] {"OnOffProperty"}, Title = "On/Off", Description = "Whether the lamp is turned on")] + public bool On + { + get => _on; + set + { + _on = value; + OnPropertyChanged(); + } + } + + private int _brightness; + + [ThingProperty(Type = new[] {"BrightnessProperty"}, Title = "Brightness", Description = "The level of light from 0-100", Minimum = 0, Maximum = 100)] - public int Brightness { get; set; } + public int Brightness + { + get => _brightness; + set + { + _brightness = value; + OnPropertyChanged(); + } + } [ThingEvent(Title = "Overheated", Type = new [] {"OverheatedEvent"}, diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs new file mode 100644 index 0000000..4148788 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs @@ -0,0 +1,79 @@ +using System; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.WebScokets +{ + public class Property + { + [Theory] + [InlineData("on", true)] + [InlineData("brightness", 10)] + public async Task SetProperties(string property, object value) + { + var host = await Program.CreateHostBuilder(null) + .StartAsync() + .ConfigureAwait(false); + var client = host.GetTestServer().CreateClient(); + var webSocketClient = host.GetTestServer().CreateWebSocketClient(); + + var uri = new UriBuilder(client.BaseAddress) + { + Scheme = "ws", + Path = "/things/lamp" + }.Uri; + var socket = await webSocketClient.ConnectAsync(uri, CancellationToken.None); + + await socket + .SendAsync(Encoding.UTF8.GetBytes($@" +{{ + ""messageType"": ""setProperty"", + ""data"": {{ + ""{property}"": {value.ToString().ToLower()} + }} +}}"), WebSocketMessageType.Text, true, + CancellationToken.None) + .ConfigureAwait(false); + + var segment = new ArraySegment(new byte[4096]); + var result = await socket.ReceiveAsync(segment, CancellationToken.None) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + var json = JToken.Parse(Encoding.UTF8.GetString(segment.Slice(0, result.Count))); + + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@" +{{ + ""messageType"": ""propertyStatus"", + ""data"": {{ + ""{property}"": {value.ToString().ToLower()} + }} +}}")); + var response = await client.GetAsync($"/things/Lamp/properties/{property}"); + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + var message = await response.Content.ReadAsStringAsync(); + json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Object); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {value.ToString().ToLower()} }}")); + } + } +} diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/WebSocketBody.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/WebSocketBody.cs new file mode 100644 index 0000000..3dca430 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/WebSocketBody.cs @@ -0,0 +1,8 @@ +namespace Mozilla.IoT.WebThing.AcceptanceTest.WebScokets +{ + public class WebSocketBody + { + public string MessageType { get; set; } + public object Data { get; set; } + } +} From f16652d1816dac75085b61241b3ea218925e762e Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Thu, 13 Feb 2020 21:50:12 +0000 Subject: [PATCH 6/9] fixes error to set read-only property --- .../Endpoints/PutProperty.cs | 7 ++ .../Extensions/IServiceExtensions.cs | 3 +- .../Properties/PropertiesIntercept.cs | 10 ++- .../IPropertyValidator.cs | 1 + src/Mozilla.IoT.WebThing/Properties.cs | 5 ++ src/Mozilla.IoT.WebThing/Property.cs | 4 +- src/Mozilla.IoT.WebThing/PropertyValidator.cs | 2 + src/Mozilla.IoT.WebThing/SetPropertyResult.cs | 3 +- .../WebSockets/SetThingProperty.cs | 21 +++++- .../Http/Properties.cs | 1 + .../Things/LampThing.cs | 3 + .../WebScokets/Property.cs | 66 +++++++++++++++++++ 12 files changed, 118 insertions(+), 8 deletions(-) diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs index 1215602..24d2f97 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs @@ -54,6 +54,13 @@ public static async Task InvokeAsync(HttpContext context) context.Response.StatusCode = (int)HttpStatusCode.BadRequest; return; } + + if (result == SetPropertyResult.ReadOnly) + { + logger.LogInformation("Read-Only Property. [Thing Name: {thingName}][Property Name: {propertyName}]", thing.Name, property); + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + return; + } await context.WriteBodyAsync(HttpStatusCode.OK, thing.ThingContext.Properties.GetProperties(property), jsonOptions) .ConfigureAwait(false); diff --git a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs index 0ab08e2..177b403 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs @@ -29,7 +29,8 @@ public static IThingCollectionBuilder AddThings(this IServiceCollection service, return new JsonSerializerOptions { PropertyNamingPolicy = opt.PropertyNamingPolicy, - DictionaryKeyPolicy = opt.PropertyNamingPolicy + DictionaryKeyPolicy = opt.PropertyNamingPolicy, + IgnoreNullValues = true }; }); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs index 6e3fa62..22fc7b5 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs @@ -30,7 +30,7 @@ public void Intercept(Thing thing, PropertyInfo propertyInfo, ThingPropertyAttri { var propertyName = thingPropertyAttribute?.Name ?? propertyInfo.Name; Properties.Add(_option.PropertyNamingPolicy.ConvertName(propertyName), new Property(GetGetMethod(propertyInfo), - GetSetMethod(propertyInfo), + GetSetMethod(propertyInfo, thingPropertyAttribute), CreateValidator(propertyInfo, thingPropertyAttribute), CreateMapper(propertyInfo.PropertyType))); } @@ -47,8 +47,14 @@ private static Func GetGetMethod(PropertyInfo property) return Expression.Lambda>(typeAs, instance).Compile(); } - private static Action GetSetMethod(PropertyInfo property) + private static Action GetSetMethod(PropertyInfo property, ThingPropertyAttribute? thingPropertyAttribute) { + if ((thingPropertyAttribute != null && thingPropertyAttribute.IsReadOnly) + || !property.CanWrite) + { + return null; + } + var instance = Expression.Parameter(typeof(object), "instance"); var value = Expression.Parameter(typeof(object), "value"); diff --git a/src/Mozilla.IoT.WebThing/IPropertyValidator.cs b/src/Mozilla.IoT.WebThing/IPropertyValidator.cs index ddf783b..293005d 100644 --- a/src/Mozilla.IoT.WebThing/IPropertyValidator.cs +++ b/src/Mozilla.IoT.WebThing/IPropertyValidator.cs @@ -2,6 +2,7 @@ namespace Mozilla.IoT.WebThing { public interface IPropertyValidator { + bool IsReadOnly { get; } bool IsValid(object value); } } diff --git a/src/Mozilla.IoT.WebThing/Properties.cs b/src/Mozilla.IoT.WebThing/Properties.cs index c681a94..3e59b1b 100644 --- a/src/Mozilla.IoT.WebThing/Properties.cs +++ b/src/Mozilla.IoT.WebThing/Properties.cs @@ -42,6 +42,11 @@ public SetPropertyResult SetProperty(string propertyName, object value) if (_properties.TryGetValue(propertyName, out var property)) { value = property.Mapper.Map(value); + if (property.Validator.IsReadOnly) + { + return SetPropertyResult.ReadOnly; + } + if (property.Validator.IsValid(value)) { property.Setter(_thing, value); diff --git a/src/Mozilla.IoT.WebThing/Property.cs b/src/Mozilla.IoT.WebThing/Property.cs index 126ba2f..7245142 100644 --- a/src/Mozilla.IoT.WebThing/Property.cs +++ b/src/Mozilla.IoT.WebThing/Property.cs @@ -11,12 +11,12 @@ public Property(Func getter, IJsonMapper mapper) { Getter = getter ?? throw new ArgumentNullException(nameof(getter)); - Setter = setter ?? throw new ArgumentNullException(nameof(setter)); + Setter = setter; Validator = validator ?? throw new ArgumentNullException(nameof(validator)); Mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } - public Action Setter { get; } + public Action? Setter { get; } public Func Getter { get; } public IPropertyValidator Validator { get; } public IJsonMapper Mapper { get; } diff --git a/src/Mozilla.IoT.WebThing/PropertyValidator.cs b/src/Mozilla.IoT.WebThing/PropertyValidator.cs index 692cd00..d78e4f6 100644 --- a/src/Mozilla.IoT.WebThing/PropertyValidator.cs +++ b/src/Mozilla.IoT.WebThing/PropertyValidator.cs @@ -20,6 +20,8 @@ public PropertyValidator(bool isReadOnly, float? minimum, float? maximum, float? _enums = enums; } + public bool IsReadOnly => _isReadOnly; + public bool IsValid(object? value) { if (_isReadOnly) diff --git a/src/Mozilla.IoT.WebThing/SetPropertyResult.cs b/src/Mozilla.IoT.WebThing/SetPropertyResult.cs index 599be3d..11a5d22 100644 --- a/src/Mozilla.IoT.WebThing/SetPropertyResult.cs +++ b/src/Mozilla.IoT.WebThing/SetPropertyResult.cs @@ -4,6 +4,7 @@ public enum SetPropertyResult { Ok, NotFound, - InvalidValue + InvalidValue, + ReadOnly } } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs index bf87071..b47fa45 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.WebSockets; using System.Text.Json; using System.Threading; @@ -31,12 +32,28 @@ public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, Js var result = thing.ThingContext.Properties.SetProperty(propertyName, property); if (result == SetPropertyResult.InvalidValue) { - _logger.LogInformation("Invalid property value"); - var response = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("error", new ErrorResponse("400 Bad Request", "Invalid Property")), options); + _logger.LogInformation("Invalid property value. [Thing: {thing}][Property Name: {propertyName}]", thing.Name, propertyName); + + var response = JsonSerializer.SerializeToUtf8Bytes( + new WebSocketResponse("error", + new ErrorResponse("400 Bad Request", "Invalid property value")), options); socket.SendAsync(response, WebSocketMessageType.Text, true, cancellationToken) .ConfigureAwait(false); } + + if (result == SetPropertyResult.ReadOnly) + { + _logger.LogInformation("Read-only property. [Thing: {thing}][Property Name: {propertyName}]", thing.Name, propertyName); + + var response = JsonSerializer.SerializeToUtf8Bytes( + new WebSocketResponse("error", + new ErrorResponse("400 Bad Request", "Read-only property")), options); + + socket.SendAsync(response, WebSocketMessageType.Text, true, cancellationToken) + .ConfigureAwait(false); + + } } return Task.CompletedTask; diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs index 6cc08f8..236dd12 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs @@ -122,6 +122,7 @@ public async Task Put(string property, object value) [Theory] [InlineData("brightness", -1, 0)] [InlineData("brightness", 101, 0)] + [InlineData("reader", 101, 0)] public async Task PutInvalidValue(string property, object value, object defaulValue) { var host = await Program.GetHost(); diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs index e2c1104..8279051 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs @@ -60,6 +60,9 @@ public int Brightness OnPropertyChanged(); } } + + public int Reader => _brightness; + [ThingEvent(Title = "Overheated", Type = new [] {"OverheatedEvent"}, diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs index 4148788..b4fb148 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs @@ -75,5 +75,71 @@ await socket .Should(json) .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {value.ToString().ToLower()} }}")); } + + [Theory] + [InlineData("brightness", -1, 0, "Invalid property value")] + [InlineData("brightness", 101, 0, "Invalid property value")] + [InlineData("reader", 50, 0, "Read-only property")] + public async Task SetPropertiesInvalidValue(string property, object value, object defaultValue, string errorMessage) + { + var host = await Program.CreateHostBuilder(null) + .StartAsync() + .ConfigureAwait(false); + var client = host.GetTestServer().CreateClient(); + var webSocketClient = host.GetTestServer().CreateWebSocketClient(); + + var uri = new UriBuilder(client.BaseAddress) + { + Scheme = "ws", + Path = "/things/lamp" + }.Uri; + var socket = await webSocketClient.ConnectAsync(uri, CancellationToken.None); + + await socket + .SendAsync(Encoding.UTF8.GetBytes($@" +{{ + ""messageType"": ""setProperty"", + ""data"": {{ + ""{property}"": {value.ToString().ToLower()} + }} +}}"), WebSocketMessageType.Text, true, + CancellationToken.None) + .ConfigureAwait(false); + + + var segment = new ArraySegment(new byte[4096]); + var result = await socket.ReceiveAsync(segment, CancellationToken.None) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + var json = JToken.Parse(Encoding.UTF8.GetString(segment.Slice(0, result.Count))); + + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@" +{{ + ""messageType"": ""error"", + ""data"": {{ + ""message"": ""{errorMessage}"", + ""status"": ""400 Bad Request"" + }} +}}")); + + var response = await client.GetAsync($"/things/Lamp/properties/{property}"); + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + var message = await response.Content.ReadAsStringAsync(); + json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Object); + FluentAssertions.Json.JsonAssertionExtensions + .Should(json) + .BeEquivalentTo(JToken.Parse($@"{{ ""{property}"": {defaultValue.ToString().ToLower()} }}")); + } } } From ce2d19215d1c45922099273c96129dac7f7c4589 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Fri, 14 Feb 2020 07:49:47 +0000 Subject: [PATCH 7/9] fixes event socket --- src/Mozilla.IoT.WebThing/EventCollection.cs | 4 +- .../Extensions/IServiceExtensions.cs | 1 + .../Generator/Events/EventIntercept.cs | 3 +- .../WebSockets/ThingObserver.cs | 8 +- .../WebScokets/Event.cs | 102 ++++++++++++++++++ .../EventCollectionTest.cs | 4 +- 6 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Event.cs diff --git a/src/Mozilla.IoT.WebThing/EventCollection.cs b/src/Mozilla.IoT.WebThing/EventCollection.cs index 5f71fde..b83798e 100644 --- a/src/Mozilla.IoT.WebThing/EventCollection.cs +++ b/src/Mozilla.IoT.WebThing/EventCollection.cs @@ -17,7 +17,7 @@ public EventCollection(int size) _events = new ConcurrentQueue(); } - public void Enqueue(Event @event) + public void Enqueue(Event @event, string name) { if (_events.Count >= _size) { @@ -33,7 +33,7 @@ public void Enqueue(Event @event) _events.Enqueue(@event); var add = Added; - add?.Invoke(this, @event); + add?.Invoke(name, @event); } public void Dequeue() diff --git a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs index 177b403..43981a1 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs @@ -30,6 +30,7 @@ public static IThingCollectionBuilder AddThings(this IServiceCollection service, { PropertyNamingPolicy = opt.PropertyNamingPolicy, DictionaryKeyPolicy = opt.PropertyNamingPolicy, + PropertyNameCaseInsensitive = opt.IgnoreCase, IgnoreNullValues = true }; }); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Events/EventIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Events/EventIntercept.cs index 8b5f0dd..e7d4551 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Events/EventIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Events/EventIntercept.cs @@ -49,7 +49,7 @@ public void Visit(Thing thing, EventInfo @event, ThingEventAttribute? eventInfo) { _eventToBind.Enqueue(@event); var name = eventInfo?.Name ?? @event.Name; - Events.Add(name, new EventCollection(_options.MaxEventSize)); + Events.Add(_options.PropertyNamingPolicy.ConvertName(name), new EventCollection(_options.MaxEventSize)); var type = @event.EventHandlerType?.GetGenericArguments()[0]!; var methodBuilder =_builder.DefineMethod($"{@event.Name}Handler", @@ -73,6 +73,7 @@ public void Visit(Thing thing, EventInfo @event, ThingEventAttribute? eventInfo) } il.Emit(OpCodes.Newobj, _createThing); + il.Emit(OpCodes.Ldstr, _options.PropertyNamingPolicy.ConvertName(name)); il.EmitCall(OpCodes.Callvirt, _addItem, null); il.Emit(OpCodes.Ret); } diff --git a/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs index 1ff85e4..2c661fb 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs @@ -35,8 +35,11 @@ public ThingObserver(ILogger logger, public async void OnEvenAdded(object sender, Event @event) { _logger.LogInformation("Event add received, going to notify Web Socket"); - var sent = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("event", @event), - _options); + var sent = JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("event", + new Dictionary + { + [sender.ToString()] = @event + }), _options); await _socket.SendAsync(sent, WebSocketMessageType.Text, true, _cancellation) .ConfigureAwait(false); @@ -65,4 +68,5 @@ await _socket.SendAsync( .ConfigureAwait(false); } } + } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Event.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Event.cs new file mode 100644 index 0000000..0c7f750 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Event.cs @@ -0,0 +1,102 @@ +using System; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.WebScokets +{ + public class Event + { + [Theory] + [InlineData("overheated")] + public async Task EventSubscription(string @event) + { + var host = await Program.CreateHostBuilder(null) + .StartAsync() + .ConfigureAwait(false); + var client = host.GetTestServer().CreateClient(); + var webSocketClient = host.GetTestServer().CreateWebSocketClient(); + + var uri = new UriBuilder(client.BaseAddress) + { + Scheme = "ws", + Path = "/things/lamp" + }.Uri; + var socket = await webSocketClient.ConnectAsync(uri, CancellationToken.None); + + await socket + .SendAsync(Encoding.UTF8.GetBytes($@" +{{ + ""messageType"": ""addEventSubscription"", + ""data"": {{ + ""{@event}"": {{}} + }} +}}"), WebSocketMessageType.Text, true, + CancellationToken.None) + .ConfigureAwait(false); + + var segment = new ArraySegment(new byte[4096]); + var result = await socket.ReceiveAsync(segment, CancellationToken.None) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + var json = JToken.Parse(Encoding.UTF8.GetString(segment.Slice(0, result.Count))); + json.Type.Should().Be(JTokenType.Object); + + var obj = (JObject)json; + + obj.GetValue("messageType", StringComparison.OrdinalIgnoreCase).Type.Should() + .Be(JTokenType.String); + obj.GetValue("messageType", StringComparison.OrdinalIgnoreCase).Value().Should() + .Be("event"); + + ((JObject)obj.GetValue("data", StringComparison.OrdinalIgnoreCase)) + .GetValue("overheated", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Object); + + + var overheated = ((JObject)((JObject)obj.GetValue("data", StringComparison.OrdinalIgnoreCase)) + .GetValue("overheated", StringComparison.OrdinalIgnoreCase)); + + overheated + .GetValue("data", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Integer); + + overheated + .GetValue("timestamp", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Date); + + var response = await client.GetAsync("/things/Lamp/events/overheated"); + + response.IsSuccessStatusCode.Should().BeTrue(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.ToString().Should().Be( "application/json"); + + var message = await response.Content.ReadAsStringAsync(); + json = JToken.Parse(message); + + json.Type.Should().Be(JTokenType.Array); + ((JArray)json).Should().HaveCountGreaterOrEqualTo(1); + + obj = ((JArray)json)[0] as JObject; + obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Object); + + ((JObject)obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase)) + .GetValue("data", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Integer); + + ((JObject)obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase)) + .GetValue("data", StringComparison.OrdinalIgnoreCase).Value().Should().Be(0); + + ((JObject)obj.GetValue("overheated", StringComparison.OrdinalIgnoreCase)) + .GetValue("timestamp", StringComparison.OrdinalIgnoreCase).Type.Should().Be(JTokenType.Date); + + } + } +} diff --git a/test/Mozilla.IoT.WebThing.Test/EventCollectionTest.cs b/test/Mozilla.IoT.WebThing.Test/EventCollectionTest.cs index de331c0..28fe18a 100644 --- a/test/Mozilla.IoT.WebThing.Test/EventCollectionTest.cs +++ b/test/Mozilla.IoT.WebThing.Test/EventCollectionTest.cs @@ -27,7 +27,7 @@ public void MaxSize(int size) { var @event = new Event(_fixture.Create()); data.AddLast(@event); - collection.Enqueue(@event); + collection.Enqueue(@event, ""); } collection.ToArray().Length.Should().Be(size); @@ -36,7 +36,7 @@ public void MaxSize(int size) var event2 = new Event(_fixture.Create()); data.AddLast(@event2); data.RemoveFirst(); - collection.Enqueue(@event2); + collection.Enqueue(@event2, ""); collection.ToArray().Length.Should().Be(size); collection.ToArray().Should().BeEquivalentTo(data); From cf3a4831e3cb5732421d2a4cefe004969327e9c6 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Fri, 14 Feb 2020 08:38:32 +0000 Subject: [PATCH 8/9] Fixes request action --- .../Actions/ActionInfo.cs | 4 +- .../Endpoints/PostAction.cs | 14 +- .../Endpoints/PostActions.cs | 19 ++- .../Generator/Actions/ActionIntercept.cs | 4 +- .../WebSockets/RequestAction.cs | 8 +- .../WebSockets/ThingObserver.cs | 7 +- .../WebScokets/Action.cs | 137 ++++++++++++++++++ 7 files changed, 170 insertions(+), 23 deletions(-) create mode 100644 test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Action.cs diff --git a/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs b/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs index 1d95973..3d35ca4 100644 --- a/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs +++ b/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs @@ -14,8 +14,8 @@ public abstract class ActionInfo internal Thing Thing { get; set; } = default!; protected abstract string ActionName { get; } - public string Href => $"/things/{Thing.Name}/actions/{ActionName}/{Id}"; - + public string Href { get; internal set; } + public DateTime TimeRequested { get; } = DateTime.UtcNow; public DateTime? TimeCompleted { get; private set; } = null; public string Status { get; private set; } = "pending"; diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs b/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs index 31f7093..7774ec2 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Actions; using Mozilla.IoT.WebThing.Converts; +using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Endpoints { @@ -30,9 +31,10 @@ public static async Task InvokeAsync(HttpContext context) return; } - var option = ThingConverter.Options; + var jsonOption = service.GetRequiredService(); + var option = service.GetRequiredService(); - var actions = await context.FromBodyAsync>(option) + var actions = await context.FromBodyAsync>(jsonOption) .ConfigureAwait(false); var actionName = context.GetRouteData("action"); @@ -57,7 +59,7 @@ public static async Task InvokeAsync(HttpContext context) { logger.LogTrace("{actionName} Action found. [Name: {thingName}]", actions, thingName); var action = (ActionInfo)JsonSerializer.Deserialize(json.GetRawText(), - actionContext.ActionType, option); + actionContext.ActionType, jsonOption); if (!action.IsValid()) { @@ -67,6 +69,8 @@ public static async Task InvokeAsync(HttpContext context) } action.Thing = thing; + var namePolicy = option.PropertyNamingPolicy; + action.Href = $"/things/{namePolicy.ConvertName(thing.Name)}/actions/{namePolicy.ConvertName(actionName)}/{action.Id}"; actionsToExecute.AddLast(action); } @@ -82,12 +86,12 @@ public static async Task InvokeAsync(HttpContext context) if (actionsToExecute.Count == 1) { - await context.WriteBodyAsync(HttpStatusCode.Created, actionsToExecute.First.Value, option) + await context.WriteBodyAsync(HttpStatusCode.Created, actionsToExecute.First.Value, jsonOption) .ConfigureAwait(false); } else { - await context.WriteBodyAsync(HttpStatusCode.Created, actionsToExecute, option) + await context.WriteBodyAsync(HttpStatusCode.Created, actionsToExecute, jsonOption) .ConfigureAwait(false); } } diff --git a/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs b/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs index f31c531..eabd938 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PostActions.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using Mozilla.IoT.WebThing.Actions; using Mozilla.IoT.WebThing.Converts; +using Mozilla.IoT.WebThing.Extensions; namespace Mozilla.IoT.WebThing.Endpoints { @@ -31,9 +32,10 @@ public static async Task InvokeAsync(HttpContext context) } context.Request.EnableBuffering(); - var option = ThingConverter.Options; + var jsonOption = service.GetRequiredService(); + var option = service.GetRequiredService(); - var actions = await context.FromBodyAsync>(option) + var actions = await context.FromBodyAsync>(jsonOption) .ConfigureAwait(false); var actionsToExecute = new LinkedList(); @@ -48,7 +50,7 @@ public static async Task InvokeAsync(HttpContext context) logger.LogTrace("{actionName} Action found. [Name: {thingName}]", actions, thingName); var action = (ActionInfo)JsonSerializer.Deserialize(json.GetRawText(), - actionContext.ActionType, option); + actionContext.ActionType, jsonOption); if (!action.IsValid()) { @@ -59,27 +61,28 @@ public static async Task InvokeAsync(HttpContext context) actionsToExecute.AddLast(action); action.Thing = thing; + var namePolicy = option.PropertyNamingPolicy; + action.Href = $"/things/{namePolicy.ConvertName(thing.Name)}/actions/{namePolicy.ConvertName(actionName)}/{action.Id}"; } foreach (var actionInfo in actionsToExecute) { logger.LogInformation("Going to execute {actionName} action. [Name: {thingName}]", actionInfo.GetActionName(), thingName); + thing.ThingContext.Actions[actionInfo.GetActionName()].Actions.Add(actionInfo.Id, actionInfo); + actionInfo.ExecuteAsync(thing, service) .ConfigureAwait(false); - - thing.ThingContext.Actions[actionInfo.GetActionName()].Actions.Add(actionInfo.Id, actionInfo); - } if (actionsToExecute.Count == 1) { - await context.WriteBodyAsync(HttpStatusCode.Created, actionsToExecute.First.Value, option) + await context.WriteBodyAsync(HttpStatusCode.Created, actionsToExecute.First.Value, jsonOption) .ConfigureAwait(false); } else { - await context.WriteBodyAsync(HttpStatusCode.Created, actionsToExecute, option) + await context.WriteBodyAsync(HttpStatusCode.Created, actionsToExecute, jsonOption) .ConfigureAwait(false); } } diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Actions/ActionIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Actions/ActionIntercept.cs index b04b00d..46dde78 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Actions/ActionIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Actions/ActionIntercept.cs @@ -19,10 +19,12 @@ public class ActionIntercept : IActionIntercept MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; private readonly ModuleBuilder _moduleBuilder; + private readonly ThingOption _option; public Dictionary Actions { get; } public ActionIntercept(ModuleBuilder moduleBuilder, ThingOption option) { + _option = option; _moduleBuilder = moduleBuilder; Actions = option.IgnoreCase ? new Dictionary(StringComparer.InvariantCultureIgnoreCase) : new Dictionary(); @@ -74,7 +76,7 @@ public void Intercept(Thing thing, MethodInfo action, ThingActionAttribute? acti CreateInputValidation(actionBuilder, inputBuilder, isValid, input); CreateExecuteAsync(actionBuilder, inputBuilder,input, action, thingType); - Actions.Add(name, new ActionContext(actionBuilder.CreateType()!)); + Actions.Add(_option.PropertyNamingPolicy.ConvertName(name), new ActionContext(actionBuilder.CreateType()!)); } private static PropertyBuilder CreateProperty(TypeBuilder builder, string fieldName, Type type) diff --git a/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs b/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs index ada453b..bf66bd6 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs @@ -33,7 +33,7 @@ public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, Js } _logger.LogTrace("{actionName} Action found. [Name: {thingName}]", actionName, thing.Name); - var actionInfo = (ActionInfo)JsonSerializer.Deserialize(json.GetBytesFromBase64(), actionContext.ActionType, options); + var actionInfo = (ActionInfo)JsonSerializer.Deserialize(json.GetRawText(), actionContext.ActionType, options); if (!actionInfo.IsValid()) { @@ -45,10 +45,12 @@ public Task ExecuteAsync(System.Net.WebSockets.WebSocket socket, Thing thing, Js _logger.LogInformation("Going to execute {actionName} action. [Name: {thingName}]", actionName, thing.Name); + var namePolicy = options.PropertyNamingPolicy; + actionInfo.Href = $"/things/{namePolicy.ConvertName(thing.Name)}/actions/{namePolicy.ConvertName(actionName)}/{actionInfo.Id}"; + + thing.ThingContext.Actions[actionInfo.GetActionName()].Actions.Add(actionInfo.Id, actionInfo); actionInfo.ExecuteAsync(thing, provider) .ConfigureAwait(false); - - thing.ThingContext.Actions[actionInfo.GetActionName()].Actions.Add(actionInfo.Id, actionInfo); } return Task.CompletedTask; diff --git a/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs index 2c661fb..9cc493f 100644 --- a/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs +++ b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs @@ -59,11 +59,10 @@ await _socket.SendAsync(sent, WebSocketMessageType.Text, true, _cancellation) public async void OnActionChange(object sender, ActionInfo action) { await _socket.SendAsync( - JsonSerializer.SerializeToUtf8Bytes(new Dictionary + JsonSerializer.SerializeToUtf8Bytes(new WebSocketResponse("actionStatus",new Dictionary { - ["messageType"] = "actionStatus", - [action.GetActionName()] = action - }, _options), + [ action.GetActionName()] = action + }), _options), WebSocketMessageType.Text, true, _cancellation) .ConfigureAwait(false); } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Action.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Action.cs new file mode 100644 index 0000000..56a9089 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Action.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Mozilla.IoT.WebThing.AcceptanceTest.WebScokets +{ + public class Action + { + [Theory] + [InlineData(50, 2_000)] + public async Task Create(int level, int duration) + { + var host = await Program.CreateHostBuilder(null) + .StartAsync() + .ConfigureAwait(false); + var client = host.GetTestServer().CreateClient(); + var webSocketClient = host.GetTestServer().CreateWebSocketClient(); + + var uri = new UriBuilder(client.BaseAddress) + { + Scheme = "ws", + Path = "/things/lamp" + }.Uri; + var socket = await webSocketClient.ConnectAsync(uri, CancellationToken.None); + await socket + .SendAsync(Encoding.UTF8.GetBytes($@" +{{ + ""messageType"": ""requestAction"", + ""data"": {{ + ""fade"": {{ + ""input"": {{ + ""level"": {level}, + ""duration"": {duration} + }} + }} + }} +}}"), WebSocketMessageType.Text, true, + CancellationToken.None) + .ConfigureAwait(false); + + var segment = new ArraySegment(new byte[4096]); + var result = await socket.ReceiveAsync(segment, CancellationToken.None) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + var json = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(segment.Slice(0, result.Count)), + new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}); + + json.MessageType.Should().Be("actionStatus"); + json.Data.Fade.Input.Should().NotBeNull(); + json.Data.Fade.Input.Level.Should().Be(level); + json.Data.Fade.Input.Duration.Should().Be(duration); + json.Data.Fade.Href.Should().StartWith("/things/lamp/actions/fade/"); + json.Data.Fade.Status.Should().Be("pending"); + json.Data.Fade.TimeRequested.Should().BeBefore(DateTime.UtcNow); + json.Data.Fade.TimeCompleted.Should().BeNull(); + + segment = new ArraySegment(new byte[4096]); + result = await socket.ReceiveAsync(segment, CancellationToken.None) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + json = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(segment.Slice(0, result.Count)), + new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}); + + json.MessageType.Should().Be("actionStatus"); + json.Data.Fade.Input.Should().NotBeNull(); + json.Data.Fade.Input.Level.Should().Be(level); + json.Data.Fade.Input.Duration.Should().Be(duration); + json.Data.Fade.Href.Should().StartWith("/things/lamp/actions/fade/"); + json.Data.Fade.Status.Should().Be("executing"); + json.Data.Fade.TimeRequested.Should().BeBefore(DateTime.UtcNow); + json.Data.Fade.TimeCompleted.Should().BeNull(); + + segment = new ArraySegment(new byte[4096]); + result = await socket.ReceiveAsync(segment, CancellationToken.None) + .ConfigureAwait(false); + + result.MessageType.Should().Be(WebSocketMessageType.Text); + result.EndOfMessage.Should().BeTrue(); + result.CloseStatus.Should().BeNull(); + + json = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(segment.Slice(0, result.Count)), + new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()}); + + json.MessageType.Should().Be("actionStatus"); + json.Data.Fade.Input.Should().NotBeNull(); + json.Data.Fade.Input.Level.Should().Be(level); + json.Data.Fade.Input.Duration.Should().Be(duration); + json.Data.Fade.Href.Should().StartWith("/things/lamp/actions/fade/"); + json.Data.Fade.Status.Should().Be("completed"); + json.Data.Fade.TimeRequested.Should().BeBefore(DateTime.UtcNow); + json.Data.Fade.TimeCompleted.Should().NotBeNull(); + + var response = await client.GetAsync($"/things/lamp/actions/fade"); + var message = await response.Content.ReadAsStringAsync(); + var json2 = JsonConvert.DeserializeObject>(message, new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + json2[0].Href.Should().StartWith("/things/lamp/actions/fade/"); + json2[0].Status.Should().NotBeNullOrEmpty(); + json2[0].Status.Should().Be("completed"); + json2[0].TimeRequested.Should().BeBefore(DateTime.UtcNow); + json2[0].TimeCompleted.Should().NotBeNull(); + json2[0].TimeCompleted.Should().BeBefore(DateTime.UtcNow); + } + + public class Message + { + public string MessageType { get; set; } + public ActionSocket Data { get; set; } + } + + public class ActionSocket + { + public Http.Action.Fade Fade { get; set; } + } + + } +} From 3dc9129168b103f2bf8f3f5b9d4589a53a4ec2c4 Mon Sep 17 00:00:00 2001 From: Rafael Lillo Date: Fri, 14 Feb 2020 11:13:15 +0000 Subject: [PATCH 9/9] Add WebSocket in sample and fixes test --- sample/MultiThing/Startup.cs | 9 +- sample/SampleThing/Startup.cs | 8 +- .../Converts/ThingConverter.cs | 9 +- .../Converter/ConvertActionIntercept.cs | 4 +- .../Converter/ConvertEventIntercept.cs | 4 +- .../Converter/ConverterPropertyIntercept.cs | 4 +- .../Properties/PropertiesIntercept.cs | 4 +- .../Http/Action.cs | 20 ++--- .../Http/Properties.cs | 5 +- .../Http/Thing.cs | 88 +++++++++++-------- .../Generator/ConverterInterceptorTest.cs | 20 ++--- .../Generator/PropertyInterceptFactoryTest.cs | 11 ++- 12 files changed, 109 insertions(+), 77 deletions(-) diff --git a/sample/MultiThing/Startup.cs b/sample/MultiThing/Startup.cs index bb44ca6..2617e59 100644 --- a/sample/MultiThing/Startup.cs +++ b/sample/MultiThing/Startup.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.WebSockets; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -30,6 +31,8 @@ public void ConfigureServices(IServiceCollection services) services.AddThings() .AddThing() .AddThing(); + + services.AddWebSockets(opt => { }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -41,7 +44,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } app.UseRouting(); - + + app.UseWebSockets(); + app.UseEndpoints(endpoints => { endpoints.MapThings(); diff --git a/sample/SampleThing/Startup.cs b/sample/SampleThing/Startup.cs index a30a0c4..ea2c027 100644 --- a/sample/SampleThing/Startup.cs +++ b/sample/SampleThing/Startup.cs @@ -1,6 +1,7 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.WebSockets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using SampleThing.Things; @@ -15,6 +16,9 @@ public void ConfigureServices(IServiceCollection services) { services.AddThings() .AddThing(); + + + services.AddWebSockets(opt => { }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -27,6 +31,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); + app.UseWebSockets(); + app.UseEndpoints(endpoints => { endpoints.MapThings(); diff --git a/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs b/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs index 787442d..0b89e1a 100644 --- a/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs +++ b/src/Mozilla.IoT.WebThing/Converts/ThingConverter.cs @@ -44,7 +44,7 @@ public override void Write(Utf8JsonWriter writer, Thing value, JsonSerializerOpt writer.WriteStartObject(); writer.WriteString("@context", value.Context); - var builder = new UriBuilder(value.Context) {Path = $"/things/{value.Name}"}; + var builder = new UriBuilder(value.Prefix) {Path = $"/things/{options.GetPropertyName(value.Name)}"}; WriteProperty(writer, "Id", builder.Uri.ToString(), options); value.ThingContext.Converter.Write(writer, value, options); @@ -52,21 +52,20 @@ public override void Write(Utf8JsonWriter writer, Thing value, JsonSerializerOpt writer.WriteStartObject(); WriteProperty(writer, "rel", "properties", options); - WriteProperty(writer, "href", $"/things/{value.Name}/properties", options); + WriteProperty(writer, "href", $"/things/{options.GetPropertyName(value.Name)}/properties", options); writer.WriteEndObject(); writer.WriteStartObject(); WriteProperty(writer, "rel", "actions", options); - WriteProperty(writer, "href", $"/things/{value.Name}/actions", options); + WriteProperty(writer, "href", $"/things/{options.GetPropertyName(value.Name)}/actions", options); writer.WriteEndObject(); writer.WriteStartObject(); WriteProperty(writer, "rel", "events", options); - WriteProperty(writer, "href", $"/things/{value.Name}/events", options); + WriteProperty(writer, "href", $"/things/{options.GetPropertyName(value.Name)}/events", options); writer.WriteEndObject(); builder.Scheme = value.Prefix.Scheme == "http" ? "ws" : "wss"; - builder.Path = $"/things/{value.Name}"; writer.WriteStartObject(); WriteProperty(writer, "rel", "alternate", options); WriteProperty(writer, "href", builder.Uri.ToString(), options); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertActionIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertActionIntercept.cs index 40eb8b3..7872c1f 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertActionIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertActionIntercept.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using System.Text.Json; using System.Threading; @@ -116,7 +116,7 @@ public void Intercept(Thing thing, MethodInfo action, ThingActionAttribute? acti _jsonWriter.StartArray("Links"); _jsonWriter.StartObject(); - _jsonWriter.PropertyWithValue("href", $"/things/{thing.Name}/actions/{_options.GetPropertyName(name)}"); + _jsonWriter.PropertyWithValue("href", $"/things/{_options.GetPropertyName(thing.Name)}/actions/{_options.GetPropertyName(name)}"); _jsonWriter.EndObject(); _jsonWriter.EndArray(); _jsonWriter.EndObject(); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertEventIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertEventIntercept.cs index d72e9ab..e58afb1 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertEventIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConvertEventIntercept.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using System.Text.Json; using Mozilla.IoT.WebThing.Attributes; @@ -59,7 +59,7 @@ public void Visit(Thing thing, EventInfo @event, ThingEventAttribute? eventInfo) _jsonWriter.StartArray("Links"); _jsonWriter.StartObject(); - _jsonWriter.PropertyWithValue( "href", $"/things/{thing.Name}/events/{_options.GetPropertyName(name)}"); + _jsonWriter.PropertyWithValue( "href", $"/things/{_options.GetPropertyName(thing.Name)}/events/{_options.GetPropertyName(name)}"); _jsonWriter.EndObject(); _jsonWriter.EndArray(); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs index b34069d..10b313e 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Converter/ConverterPropertyIntercept.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using System.Text.Json; using Mozilla.IoT.WebThing.Attributes; @@ -83,7 +83,7 @@ public void Intercept(Thing thing, PropertyInfo propertyInfo, ThingPropertyAttri _jsonWriter.StartObject(); _jsonWriter.PropertyWithValue("href", - $"/things/{thing.Name}/properties/{_options.GetPropertyName(propertyName)}"); + $"/things/{_options.GetPropertyName(thing.Name)}/properties/{_options.GetPropertyName(propertyName)}"); _jsonWriter.EndObject(); _jsonWriter.EndArray(); diff --git a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs index 22fc7b5..4c4f2cf 100644 --- a/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs +++ b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; @@ -18,7 +18,7 @@ public PropertiesIntercept(ThingOption option) { _option = option ?? throw new ArgumentNullException(nameof(option)); Properties = option.IgnoreCase ? new Dictionary(StringComparer.InvariantCultureIgnoreCase) - : new Dictionary(); + : new Dictionary(); } public void Before(Thing thing) diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Action.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Action.cs index 8e91675..a1a85dd 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Action.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Action.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -45,7 +45,7 @@ public async Task Create(int level, int duration) json.Input.Should().NotBeNull(); json.Input.Level.Should().Be(level); json.Input.Duration.Should().Be(duration); - json.Href.Should().StartWith("/things/Lamp/actions/fade/"); + json.Href.Should().StartWith("/things/lamp/actions/fade/"); json.Status.Should().NotBeNullOrEmpty(); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); } @@ -81,7 +81,7 @@ public async Task CreateInSpecificUrl(int level, int duration) json.Input.Should().NotBeNull(); json.Input.Level.Should().Be(level); json.Input.Duration.Should().Be(duration); - json.Href.Should().StartWith("/things/Lamp/actions/fade/"); + json.Href.Should().StartWith("/things/lamp/actions/fade/"); json.Status.Should().NotBeNullOrEmpty(); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); } @@ -172,7 +172,7 @@ public async Task LongRunner() var response = await client.PostAsync("/things/Lamp/actions", new StringContent($@" {{ - ""LongRun"": {{ + ""longRun"": {{ }} }}")); response.IsSuccessStatusCode.Should().BeTrue(); @@ -185,20 +185,20 @@ public async Task LongRunner() ContractResolver = new CamelCasePropertyNamesContractResolver() }); - json.Href.Should().StartWith("/things/Lamp/actions/LongRun/"); + json.Href.Should().StartWith("/things/lamp/actions/longRun/"); json.Status.Should().NotBeNullOrEmpty(); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); await Task.Delay(3_000); - response = await client.GetAsync($"/things/Lamp/actions/LongRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); + response = await client.GetAsync($"/things/lamp/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); message = await response.Content.ReadAsStringAsync(); json = JsonConvert.DeserializeObject(message, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - json.Href.Should().StartWith("/things/Lamp/actions/LongRun/"); + json.Href.Should().StartWith("/things/lamp/actions/longRun/"); json.Status.Should().NotBeNullOrEmpty(); json.Status.Should().Be("completed"); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); @@ -229,14 +229,14 @@ public async Task CancelAction() ContractResolver = new CamelCasePropertyNamesContractResolver() }); - json.Href.Should().StartWith("/things/Lamp/actions/LongRun/"); + json.Href.Should().StartWith("/things/lamp/actions/longRun/"); json.Status.Should().NotBeNullOrEmpty(); json.TimeRequested.Should().BeBefore(DateTime.UtcNow); - response = await client.DeleteAsync($"/things/Lamp/actions/LongRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); + response = await client.DeleteAsync($"/things/lamp/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); response.StatusCode.Should().Be(HttpStatusCode.NoContent); - response = await client.GetAsync($"/things/Lamp/actions/LongRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); + response = await client.GetAsync($"/things/lamp/actions/longRun/{json.Href.Substring(json.Href.LastIndexOf('/') + 1)}"); response.StatusCode.Should().Be(HttpStatusCode.NotFound); } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs index 236dd12..a1795b0 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Properties.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using AutoFixture; @@ -40,7 +40,8 @@ public async Task GetAll() .BeEquivalentTo(JToken.Parse(@" { ""on"": false, - ""brightness"": 0 + ""brightness"": 0, + ""reader"": 0 } ")); } diff --git a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs index 470b2b3..3d613f7 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Http/Thing.cs @@ -1,9 +1,8 @@ -using System.Net; +using System.Net; using System.Threading.Tasks; using AutoFixture; using FluentAssertions; using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; using Xunit; @@ -33,7 +32,7 @@ public async Task GetAll() [ { ""@context"": ""https://iot.mozilla.org/schemas"", - ""id"": ""https://iot.mozilla.org/things/Lamp"", + ""id"": ""http://localhost/things/lamp"", ""title"": ""My Lamp"", ""description"": ""A web connected lamp"", ""@type"": [ @@ -45,11 +44,11 @@ public async Task GetAll() ""title"": ""On/Off"", ""description"": ""Whether the lamp is turned on"", ""readOnly"": false, - ""@type"": ""OnOffProperty"", ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", ""links"": [ { - ""href"": ""/things/Lamp/properties/on"" + ""href"": ""/things/lamp/properties/on"" } ] }, @@ -57,13 +56,20 @@ public async Task GetAll() ""title"": ""Brightness"", ""description"": ""The level of light from 0-100"", ""readOnly"": false, + ""type"": ""integer"", ""@type"": ""BrightnessProperty"", ""minimum"": 0, ""maximum"": 100, - ""type"": ""integer"", ""links"": [ { - ""href"": ""/things/Lamp/properties/brightness"" + ""href"": ""/things/lamp/properties/brightness"" + } + ] + }, + ""reader"": { + ""links"": [ + { + ""href"": ""/things/lamp/properties/reader"" } ] } @@ -90,18 +96,18 @@ public async Task GetAll() }, ""links"": [ { - ""href"": ""/things/Lamp/actions/fade"" + ""href"": ""/things/lamp/actions/fade"" } ] }, - ""longRun"":{ + ""longRun"": { ""input"": { ""type"": ""object"", ""properties"": {} }, ""links"": [ { - ""href"": ""/things/Lamp/actions/longRun"" + ""href"": ""/things/lamp/actions/longRun"" } ] } @@ -114,38 +120,41 @@ public async Task GetAll() ""type"": ""integer"", ""links"": [ { - ""href"": ""/things/Lamp/events/overheated"" + ""href"": ""/things/lamp/events/overheated"" } ] }, ""otherEvent"": { ""title"": ""OtherEvent"", ""type"": ""string"", - ""links"": [{ - ""href"": ""/things/Lamp/events/otherEvent"" - }] + ""links"": [ + { + ""href"": ""/things/lamp/events/otherEvent"" + } + ] } }, ""links"": [ { ""rel"": ""properties"", - ""href"": ""/things/Lamp/properties"" + ""href"": ""/things/lamp/properties"" }, { ""rel"": ""actions"", - ""href"": ""/things/Lamp/actions"" + ""href"": ""/things/lamp/actions"" }, { ""rel"": ""events"", - ""href"": ""/things/Lamp/events"" + ""href"": ""/things/lamp/events"" }, { ""rel"": ""alternate"", - ""href"": ""ws://iot.mozilla.org:443/things/Lamp"" + ""href"": ""ws://localhost/things/lamp"" } ] } -]")); +] +")); } [Fact] @@ -168,7 +177,7 @@ public async Task Get() .BeEquivalentTo(JToken.Parse(@" { ""@context"": ""https://iot.mozilla.org/schemas"", - ""id"": ""https://iot.mozilla.org/things/Lamp"", + ""id"": ""http://localhost/things/lamp"", ""title"": ""My Lamp"", ""description"": ""A web connected lamp"", ""@type"": [ @@ -180,11 +189,11 @@ public async Task Get() ""title"": ""On/Off"", ""description"": ""Whether the lamp is turned on"", ""readOnly"": false, - ""@type"": ""OnOffProperty"", ""type"": ""boolean"", + ""@type"": ""OnOffProperty"", ""links"": [ { - ""href"": ""/things/Lamp/properties/on"" + ""href"": ""/things/lamp/properties/on"" } ] }, @@ -192,13 +201,20 @@ public async Task Get() ""title"": ""Brightness"", ""description"": ""The level of light from 0-100"", ""readOnly"": false, + ""type"": ""integer"", ""@type"": ""BrightnessProperty"", ""minimum"": 0, ""maximum"": 100, - ""type"": ""integer"", ""links"": [ { - ""href"": ""/things/Lamp/properties/brightness"" + ""href"": ""/things/lamp/properties/brightness"" + } + ] + }, + ""reader"": { + ""links"": [ + { + ""href"": ""/things/lamp/properties/reader"" } ] } @@ -225,18 +241,18 @@ public async Task Get() }, ""links"": [ { - ""href"": ""/things/Lamp/actions/fade"" + ""href"": ""/things/lamp/actions/fade"" } ] }, - ""longRun"":{ + ""longRun"": { ""input"": { ""type"": ""object"", ""properties"": {} }, ""links"": [ { - ""href"": ""/things/Lamp/actions/longRun"" + ""href"": ""/things/lamp/actions/longRun"" } ] } @@ -249,34 +265,36 @@ public async Task Get() ""type"": ""integer"", ""links"": [ { - ""href"": ""/things/Lamp/events/overheated"" + ""href"": ""/things/lamp/events/overheated"" } ] }, ""otherEvent"": { ""title"": ""OtherEvent"", ""type"": ""string"", - ""links"": [{ - ""href"": ""/things/Lamp/events/otherEvent"" - }] + ""links"": [ + { + ""href"": ""/things/lamp/events/otherEvent"" + } + ] } }, ""links"": [ { ""rel"": ""properties"", - ""href"": ""/things/Lamp/properties"" + ""href"": ""/things/lamp/properties"" }, { ""rel"": ""actions"", - ""href"": ""/things/Lamp/actions"" + ""href"": ""/things/lamp/actions"" }, { ""rel"": ""events"", - ""href"": ""/things/Lamp/events"" + ""href"": ""/things/lamp/events"" }, { ""rel"": ""alternate"", - ""href"": ""ws://iot.mozilla.org:443/things/Lamp"" + ""href"": ""ws://localhost/things/lamp"" } ] } diff --git a/test/Mozilla.IoT.WebThing.Test/Generator/ConverterInterceptorTest.cs b/test/Mozilla.IoT.WebThing.Test/Generator/ConverterInterceptorTest.cs index 23e7503..8a6e762 100644 --- a/test/Mozilla.IoT.WebThing.Test/Generator/ConverterInterceptorTest.cs +++ b/test/Mozilla.IoT.WebThing.Test/Generator/ConverterInterceptorTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text.Json; using AutoFixture; @@ -52,7 +52,7 @@ public void Serialize() .BeEquivalentTo(JToken.Parse(@" { ""@context"": ""https://iot.mozilla.org/schemas"", - ""id"": ""https://iot.mozilla.org/things/Lamp"", + ""id"": ""http://localhost/things/lamp"", ""title"": ""My Lamp"", ""description"": ""A web connected lamp"", ""@type"": [ @@ -69,7 +69,7 @@ public void Serialize() ""type"": ""boolean"", ""links"": [ { - ""href"": ""/things/Lamp/properties/on"" + ""href"": ""/things/lamp/properties/on"" } ] }, @@ -84,7 +84,7 @@ public void Serialize() ""type"": ""integer"", ""links"": [ { - ""href"": ""/things/Lamp/properties/brightness"" + ""href"": ""/things/lamp/properties/brightness"" } ] } @@ -111,7 +111,7 @@ public void Serialize() }, ""links"": [ { - ""href"": ""/things/Lamp/actions/fade"" + ""href"": ""/things/lamp/actions/fade"" } ] } @@ -124,7 +124,7 @@ public void Serialize() ""type"": ""number"", ""links"": [ { - ""href"": ""/things/Lamp/events/overheated"" + ""href"": ""/things/lamp/events/overheated"" } ] } @@ -132,19 +132,19 @@ public void Serialize() ""links"": [ { ""rel"": ""properties"", - ""href"": ""/things/Lamp/properties"" + ""href"": ""/things/lamp/properties"" }, { ""rel"": ""actions"", - ""href"": ""/things/Lamp/actions"" + ""href"": ""/things/lamp/actions"" }, { ""rel"": ""events"", - ""href"": ""/things/Lamp/events"" + ""href"": ""/things/lamp/events"" }, { ""rel"": ""alternate"", - ""href"": ""ws://iot.mozilla.org:443/things/Lamp"" + ""href"": ""ws://localhost/things/lamp"" } ] } diff --git a/test/Mozilla.IoT.WebThing.Test/Generator/PropertyInterceptFactoryTest.cs b/test/Mozilla.IoT.WebThing.Test/Generator/PropertyInterceptFactoryTest.cs index 0342cb5..1a0065a 100644 --- a/test/Mozilla.IoT.WebThing.Test/Generator/PropertyInterceptFactoryTest.cs +++ b/test/Mozilla.IoT.WebThing.Test/Generator/PropertyInterceptFactoryTest.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using AutoFixture; using FluentAssertions; using Mozilla.IoT.WebThing.Attributes; @@ -19,7 +19,10 @@ public PropertyInterceptFactoryTest() { _fixture = new Fixture(); _thing = new LampThing(); - _factory = new PropertiesInterceptFactory(_thing, new ThingOption()); + _factory = new PropertiesInterceptFactory(_thing, new ThingOption + { + + }); } [Fact] @@ -44,10 +47,10 @@ public void GetValue() var properties = _factory.Create(); var values = properties.GetProperties(); - values.ContainsKey(nameof(LampThing.Id)).Should().BeTrue(); + values.ContainsKey("id").Should().BeTrue(); values.ContainsKey("test").Should().BeTrue(); - values[nameof(LampThing.Id)].Should().Be(id); + values["id"].Should().Be(id); values["test"].Should().Be(value); properties.GetProperties(nameof(LampThing.Id))[nameof(LampThing.Id)].Should().Be(id);