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/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/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/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 new file mode 100644 index 0000000..c656a04 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/ActionCollection.cs @@ -0,0 +1,56 @@ +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? Change; + + public ActionCollection() + { + _actions = new ConcurrentDictionary(); + } + + public void Add(Guid id, ActionInfo actionInfo) + { + _actions.TryAdd(id, actionInfo); + + actionInfo.StatusChanged += OnStatusChange; + + var change = Change; + change?.Invoke(this, actionInfo); + } + + public bool TryGetValue(Guid id, out ActionInfo? action) + => _actions.TryGetValue(id, out action); + + public bool TryRemove(Guid id, out ActionInfo 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(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Mozilla.IoT.WebThing/ActionContext.cs b/src/Mozilla.IoT.WebThing/ActionContext.cs index 1c2e1d2..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 { @@ -12,6 +10,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/Actions/ActionInfo.cs b/src/Mozilla.IoT.WebThing/Actions/ActionInfo.cs index b58bb9b..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"; @@ -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 41e5cf2..a92e716 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 @@ -20,8 +22,8 @@ public Context(IThingConverter converter, public IThingConverter Converter { get; } public IProperties Properties { get; } - public Dictionary Events { get; } public Dictionary Actions { get; } + public ConcurrentDictionary Sockets { get; } = new ConcurrentDictionary(); } } 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/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..9ea4624 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; @@ -52,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 87e24e9..50ba07f 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 { @@ -47,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/PostAction.cs b/src/Mozilla.IoT.WebThing/Endpoints/PostAction.cs index b2cb861..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); } @@ -77,17 +81,17 @@ 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) { - 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 854d3ff..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.TryAdd(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/Endpoints/PutProperty.cs b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs index 03ed03d..24d2f97 100644 --- a/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs +++ b/src/Mozilla.IoT.WebThing/Endpoints/PutProperty.cs @@ -32,12 +32,14 @@ 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 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[property]); + var result = thing.ThingContext.Properties.SetProperty(property, json.GetProperty(property)); if (result == SetPropertyResult.NotFound) { @@ -52,8 +54,15 @@ 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), ThingConverter.Options) + await context.WriteBodyAsync(HttpStatusCode.OK, thing.ThingContext.Properties.GetProperties(property), jsonOptions) .ConfigureAwait(false); } } diff --git a/src/Mozilla.IoT.WebThing/EventCollection.cs b/src/Mozilla.IoT.WebThing/EventCollection.cs index 006d0f0..b83798e 100644 --- a/src/Mozilla.IoT.WebThing/EventCollection.cs +++ b/src/Mozilla.IoT.WebThing/EventCollection.cs @@ -1,20 +1,23 @@ +using System; using System.Collections.Concurrent; 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 event EventHandler? Added; + public EventCollection(int size) { _size = size; _events = new ConcurrentQueue(); } - public void Enqueue(Event @event) + public void Enqueue(Event @event, string name) { if (_events.Count >= _size) { @@ -28,6 +31,9 @@ public void Enqueue(Event @event) } _events.Enqueue(@event); + + var add = Added; + add?.Invoke(name, @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..43981a1 100644 --- a/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs +++ b/src/Mozilla.IoT.WebThing/Extensions/IServiceExtensions.cs @@ -1,11 +1,17 @@ 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 { 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) { @@ -16,9 +22,45 @@ 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, + DictionaryKeyPolicy = opt.PropertyNamingPolicy, + PropertyNameCaseInsensitive = opt.IgnoreCase, + IgnoreNullValues = true + }; + }); + + service.AddSingleton(); + service.AddSingleton(); + service.AddSingleton(); + + service.AddScoped(); + service.AddScoped(provider => provider.GetRequiredService().Observer); + + 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; } } + + public class ThingObserverResolver + { + public ThingObserver Observer { get; set; } = default!; + } + } 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/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/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/Factories/Generator/Properties/PropertiesIntercept.cs b/src/Mozilla.IoT.WebThing/Factories/Generator/Properties/PropertiesIntercept.cs index a9070ba..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; @@ -12,11 +12,13 @@ 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(); + : new Dictionary(); } public void Before(Thing thing) @@ -27,8 +29,8 @@ 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), - GetSetMethod(propertyInfo), + Properties.Add(_option.PropertyNamingPolicy.ConvertName(propertyName), new Property(GetGetMethod(propertyInfo), + GetSetMethod(propertyInfo, thingPropertyAttribute), CreateValidator(propertyInfo, thingPropertyAttribute), CreateMapper(propertyInfo.PropertyType))); } @@ -45,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/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/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/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/Properties.cs b/src/Mozilla.IoT.WebThing/Properties.cs index 1706d20..3e59b1b 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) @@ -40,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/Thing.cs b/src/Mozilla.IoT.WebThing/Thing.cs index fc66903..b434604 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 new file mode 100644 index 0000000..65b0ece --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/AddEventSubscription.cs @@ -0,0 +1,29 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +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) + { + var observer = provider.GetRequiredService(); + foreach (var (@event, collection) in thing.ThingContext.Events) + { + if (data.TryGetProperty(@event, out _)) + { + collection.Added += observer.OnEvenAdded; + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs b/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs new file mode 100644 index 0000000..5d8745f --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/IWebSocketAction.cs @@ -0,0 +1,15 @@ +using System; +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..bf66bd6 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/RequestAction.cs @@ -0,0 +1,59 @@ +using System; +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.GetRawText(), 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); + + 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); + } + + 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..b47fa45 --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/SetThingProperty.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +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) + { + foreach (var propertyName in thing.ThingContext.Properties.PropertiesNames) + { + if (!data.TryGetProperty(options.PropertyNamingPolicy.ConvertName(propertyName), out var property)) + { + continue; + } + + var result = thing.ThingContext.Properties.SetProperty(propertyName, property); + if (result == SetPropertyResult.InvalidValue) + { + _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/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs new file mode 100644 index 0000000..9cc493f --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/ThingObserver.cs @@ -0,0 +1,71 @@ +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", + new Dictionary + { + [sender.ToString()] = @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("propertyStatus", + _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 WebSocketResponse("actionStatus",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 new file mode 100644 index 0000000..691f6ea --- /dev/null +++ b/src/Mozilla.IoT.WebThing/WebSockets/WebSocket.cs @@ -0,0 +1,178 @@ +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.GetRequiredService>().Value; + + var observer = new ThingObserver(service.GetRequiredService>(), + jsonOptions, socket, cancellation, thing); + + try + { + BindActions(thing, observer); + BindPropertyChanged(thing, observer); + + while (!socket.CloseStatus.HasValue && !cancellation.IsCancellationRequested) + { + if (buffer != null) + { + s_pool.Return(buffer, true); + } + + buffer = s_pool.Rent(webSocketOption.ReceiveBufferSize); + var segment = new ArraySegment(buffer); + var received = await socket + .ReceiveAsync(segment, cancellation) + .ConfigureAwait(false); + + var json = JsonSerializer.Deserialize(segment.Slice(0, received.Count), jsonOptions); + + if (!json.TryGetProperty("messageType", out var messageType)) + { + logger.LogInformation("Web Socket request without messageType"); + await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + continue; + } + + if (!json.TryGetProperty("data", out var data)) + { + 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(messageType.GetString(), out var action)) + { + logger.LogInformation("Invalid Message Type: {messageType}", messageType.GetString()); + await socket.SendAsync(s_error, WebSocketMessageType.Text, true, cancellation) + .ConfigureAwait(false); + continue; + } + + try + { + using var scope = service.CreateScope(); + scope.ServiceProvider.GetRequiredService().Observer = observer; + 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}", 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); + } + + UnbindActions(thing, observer); + UnbindPropertyChanged(thing, observer); + UnbindEvent(thing, observer); + } + + private static void BindActions(Thing thing, ThingObserver observer) + { + foreach (var (_, actionContext) in thing.ThingContext.Actions) + { + actionContext.Actions.Change += observer.OnActionChange; + } + } + + private static void BindPropertyChanged(Thing thing, ThingObserver observer) + => thing.PropertyChanged += observer.OnPropertyChanged; + + private static void UnbindActions(Thing thing, ThingObserver observer) + { + foreach (var (_, actionContext) in thing.ThingContext.Actions) + { + 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/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; } + } +} 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/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..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; @@ -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"); @@ -41,7 +40,8 @@ public async Task GetAll() .BeEquivalentTo(JToken.Parse(@" { ""on"": false, - ""brightness"": 0 + ""brightness"": 0, + ""reader"": 0 } ")); } @@ -51,8 +51,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 +71,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()}"); @@ -125,10 +123,10 @@ 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.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 +152,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..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; @@ -14,8 +13,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"); @@ -34,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"": [ @@ -46,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"" } ] }, @@ -58,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"" } ] } @@ -91,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"" } ] } @@ -115,45 +120,47 @@ 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] 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"); @@ -170,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"": [ @@ -182,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"" } ] }, @@ -194,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"" } ] } @@ -227,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"" } ] } @@ -251,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"" } ] } @@ -289,8 +305,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(); + } } } 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 c117a8e..8279051 100644 --- a/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/Things/LampThing.cs @@ -31,16 +31,38 @@ 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; } - - [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(); + } + } + + public int Reader => _brightness; + [ThingEvent(Title = "Overheated", Type = new [] {"OverheatedEvent"}, 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; } + } + + } +} 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.AcceptanceTest/WebScokets/Property.cs b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs new file mode 100644 index 0000000..b4fb148 --- /dev/null +++ b/test/Mozilla.IoT.WebThing.AcceptanceTest/WebScokets/Property.cs @@ -0,0 +1,145 @@ +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()} }}")); + } + + [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()} }}")); + } + } +} 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; } + } +} 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); 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);