Skip to content

Commit 32b8e49

Browse files
Update version and Trigger CI
1 parent ec77e39 commit 32b8e49

File tree

8 files changed

+151
-30
lines changed

8 files changed

+151
-30
lines changed

Directory.Build.targets

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232

3333
<PackageReference Update="MQTTnet" Version="4.3.1.873"/>
3434
<PackageReference Update="MQTTnet.Extensions.ManagedClient" Version="4.3.1.873"/>
35-
<!--<PackageReference Update="MQTTnet.AspNetCore" Version="4.3.1.873"/>-->
3635

3736
<FrameworkReference Update="Microsoft.AspNetCore.App" Version="$(dotnetVersion)"/>
3837
<PackageReference Update="Microsoft.Extensions.Logging" Version="$(dotnetVersion)" />

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8.5.1
1+
8.6.0

src/Moryx.Communication.Mqtt.Endpoints/Moryx.Communication.Mqtt.Endpoints.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
<TargetFramework>net8.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
7+
<PackageReadmeFile>README.md</PackageReadmeFile>
78
</PropertyGroup>
89

910
<ItemGroup>
1011
<ProjectReference Include="..\Moryx.Communication.Mqtt\Moryx.Communication.Mqtt.csproj" />
1112
</ItemGroup>
1213

13-
</Project>
14+
<ItemGroup>
15+
<None Include="README.md" Pack="true" PackagePath="" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,66 @@
11
# ResourceRpcEndpoint
2-
- Endpoint to remote call a procedure/method on a resource
2+
- Endpoint to remote call a procedure/method on a resource.
33

44
When the endpoint receives a request(Message) the response will be sent to :
55
1. `MqttApplicationMessage.ResponseTopic` if the request (message) has a response topic.
66
2. `resources/{identifier}/invoked/{methodName}` when the request (Message) doesn't have a ResponseTopic
77

8-
When a client sends a message to the following topic : `"resources/{identifier}/invoke/{methodName}"` , for example `resources/1236545/invoke/ChangeCapabilities`. The `ResourceRpcEndpoint`will be triggered and the matching Resource with the `Identifier` or `Name` `1236545` and the method that matches the name `ChangeCapabilities` will be invoked.
8+
## Why do we need a ResourceRpcEndpoint ?
9+
10+
### Scenario :
11+
Here is a scenario where i need the `ResourceRpcEndpoint`. We have 2 Moryx instances running on different machine. One is a Raspberry pi and one is a desktop, both on the same WiFi network. The Raspberry pi only has one moryx resource called `Camera` with a physical camera connected. And the desktop has all the station of the `Machine` (Note: the physical machine is an entire factory floor). At one particular station `Printing` we need to take pictures. For that the `Printing` station needs to send a `Take a picture and send it to me` command to the `Camera` (Which is on the Raspberry pi).
12+
13+
## Example:
14+
Given the following Resource:
15+
16+
```cs
17+
public IMyPublicMethods
18+
{
19+
void MethodWithParams(int value){
20+
//code
21+
}
22+
23+
int MethodWithResult()
24+
{
25+
return 1;
26+
}
27+
}
28+
29+
[ResourceAvailableAs(typeof(IMyPublicMethod)))]
30+
public class ExampleResource : Resource, IMyPublicMethods
31+
{
32+
public long Id => 1; // example
33+
public void MethodWithParams(int value)
34+
{
35+
//code
36+
}
37+
38+
public int MethodWithResult()
39+
{
40+
return 1
41+
}
42+
// codes...
43+
}
44+
```
45+
46+
- When a client sends a message to the following topic : `resources/1/invoke/MethodWithParams` with this payload
47+
```json
48+
{
49+
"Value" : 152
50+
}
51+
```
52+
- The `ResourceRpcEndpoint` will be triggered and the method `MethodWithParams` will be invoked. The payload inside the message is used as value for the method parameters , in this case `Value = 123` the name matches the parameter name (not case sensitif). The response message has the following structure:
53+
```json
54+
{
55+
"Value" = object,
56+
"ValueType" = EntityEntryValueType // EntryConvert.TransformType(methodToInvoke.ReturnType)
57+
}
58+
```
59+
60+
- When a client sends a message to the following topic : `resources/1/invoke/MethodWithResult` with an empty payload
61+
and a response topic `device-125/resources/1/MethodWithResult/response`
62+
63+
- The `ResourceRpcEndpoint` will be triggered and the method `MethodWithResult` will be invoked. And the return value `1` will be sent back to the broker using the topic `device-125/resources/1/MethodWithResult/response`.
64+
65+
66+
Note : All serialization a done via `MqttMessageSerialization`

src/Moryx.Communication.Mqtt.Endpoints/ResourceRpcEndpoint.cs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2025, Phoenix Contact GmbH & Co. KG
22
// Licensed under the Apache License, Version 2.0
33

4+
using System.Reflection;
45
using Microsoft.Extensions.Logging;
56
using Moryx.AbstractionLayer.Identity;
67
using Moryx.AbstractionLayer.Resources;
@@ -21,6 +22,7 @@ public class ResourceRpcEndpoint(
2122
MqttClientUserOptions options)
2223
: IMqttEndpoint
2324
{
25+
private const string message = "No resource or method found with the given Identifier:`{id}`, method:`{method}`. Task and Task<T> are not supported return types.";
2426
public void Map(IMqttRouteBuilder routeBuilder)
2527
{
2628
routeBuilder.MapGet("resources/{identifier}/invoke/{methodName}", context =>
@@ -33,29 +35,39 @@ public void Map(IMqttRouteBuilder routeBuilder)
3335
var resource = resourceManagement.GetResource<IResource>(x =>
3436
x is IIdentifiableObject obj && obj.Identity.Identifier == identifier
3537
|| x.Name == identifier);
36-
return FindAndInvokeMethod(context, messageBuilder, identifier, methodName, resource);
38+
var methodToInvoke = FindMethod(resource, methodName, identifier);
39+
return methodToInvoke is null
40+
? GetExceptionMessage(messageBuilder, string.Format(message, identifier, methodName))
41+
: InvokeMethod(context, messageBuilder, resource, methodToInvoke);
3742
});
3843
}
3944

40-
private MqttApplicationMessage? FindAndInvokeMethod(
41-
MqttEndpointContext context,
42-
MqttApplicationMessageBuilder responseBuilder,
43-
string? identifier,
44-
string methodName,
45-
IResource? resource)
45+
private MethodInfo? FindMethod(IResource resource, string methodName, string? identifier)
4646
{
4747
var methodToInvoke = resource
48-
?.GetType()
49-
.GetInterfaces()
50-
.Select(i => i.GetMethod(methodName))
51-
.FirstOrDefault(x => x is not null);
52-
48+
?.GetType()
49+
.GetInterfaces()
50+
.Select(i =>
51+
i.GetMethods()
52+
.Where(m =>
53+
m.ReturnType == typeof(Task)
54+
|| (m.ReturnType.IsGenericType && m.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)))
55+
.FirstOrDefault(x => x.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase)))
56+
.FirstOrDefault(x => x is not null);
5357
if (resource is null || methodToInvoke is null)
5458
{
55-
logger.Log(LogLevel.Error, "No resource or method found with the given Identifier:`{id}`, method:`{method}`.", identifier, methodName);
59+
logger.Log(LogLevel.Error, message, identifier, methodName);
5660
return null;
5761
}
62+
return methodToInvoke;
63+
}
5864

65+
private MqttApplicationMessage InvokeMethod(
66+
MqttEndpointContext context,
67+
MqttApplicationMessageBuilder responseBuilder,
68+
IResource resource,
69+
MethodInfo methodToInvoke)
70+
{
5971
var methodToInvokeParameters = methodToInvoke.GetParameters();
6072
var parameters = new object?[methodToInvokeParameters.Length];
6173

@@ -66,16 +78,27 @@ public void Map(IMqttRouteBuilder routeBuilder)
6678
// get the values of the method's parameters from the body of the message
6779
parameters[i] = context.FromBody(methodToInvokeParameters[i].Name!, methodToInvokeParameters[i].ParameterType);
6880
}
69-
catch (Exception ex)
81+
catch (InvalidCastException ex)
7082
{
7183
logger.LogError(ex,
7284
"Impossible to get Method '{method}' parameter '{parameter}' from the Payload.",
73-
methodName,
85+
methodToInvoke.Name,
7486
methodToInvokeParameters[i].Name);
75-
continue;
87+
return GetExceptionMessage(responseBuilder, ex.Message);
7688
}
7789
}
78-
var methodResult = methodToInvoke.Invoke(resource, parameters);
90+
91+
object? methodResult;
92+
93+
try
94+
{
95+
methodResult = methodToInvoke.Invoke(resource, parameters);
96+
}
97+
catch (Exception ex)
98+
{
99+
return GetExceptionMessage(responseBuilder, ex.Message);
100+
}
101+
79102
if (methodResult?.GetType() == typeof(void))
80103
{
81104
responseBuilder.WithPayload(MqttMessageSerialization.GetJsonPayload(new { }, options.JsonSerializerOptions));
@@ -95,5 +118,16 @@ public void Map(IMqttRouteBuilder routeBuilder)
95118
}, options.JsonSerializerOptions) ?? string.Empty;
96119
return responseBuilder.WithPayload(jsonPayload).Build();
97120
}
121+
122+
private MqttApplicationMessage GetExceptionMessage(MqttApplicationMessageBuilder builder, string exception)
123+
{
124+
var payload = new MethodInvocationResult
125+
{
126+
Value = new { Message = exception },
127+
ValueType = EntryValueType.Exception
128+
};
129+
var json = MqttMessageSerialization.GetJsonPayload(payload, options.JsonSerializerOptions);
130+
return builder.WithPayload(json).Build();
131+
}
98132
}
99133

src/Moryx.Communication.Mqtt.Services/Moryx.Communication.Mqtt.Services.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
<TargetFramework>net8.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
7+
<PackageReadmeFile>README.md</PackageReadmeFile>
78
</PropertyGroup>
89

910
<ItemGroup>
1011
<ProjectReference Include="..\Moryx.Communication.Mqtt\Moryx.Communication.Mqtt.csproj" />
1112
</ItemGroup>
1213

13-
</Project>
14+
<ItemGroup>
15+
<None Include="README.md" Pack="true" PackagePath="" />
16+
</ItemGroup>
17+
</Project>

src/Moryx.Communication.Mqtt/Moryx.Communication.Mqtt.csproj

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@
44
<TargetFramework>net8.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
7+
<PackageReadmeFile>README.md</PackageReadmeFile>
78
</PropertyGroup>
89

910
<ItemGroup>
10-
<FrameworkReference Include="Microsoft.AspNetCore.App" />
11-
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
12-
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
11+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
12+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
13+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
1314

14-
<PackageReference Include="MQTTnet" />
15-
<PackageReference Include="MQTTnet.Extensions.ManagedClient" />
15+
<PackageReference Include="MQTTnet" />
16+
<PackageReference Include="MQTTnet.Extensions.ManagedClient" />
1617
</ItemGroup>
1718

1819
<ItemGroup>
1920
<ProjectReference Include="..\Moryx.AbstractionLayer\Moryx.AbstractionLayer.csproj" />
2021
<ProjectReference Include="..\Moryx.Runtime\Moryx.Runtime.csproj" />
2122
</ItemGroup>
22-
23-
</Project>
23+
<ItemGroup>
24+
<None Include="README.md" Pack="true" PackagePath="" />
25+
</ItemGroup>
26+
</Project>

src/Moryx.Communication.Mqtt/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ Add your configuration file `Moryx.Mqtt.Config.json`, inside the default MORYX C
6565
}
6666
```
6767

68+
## Serialization
69+
The serialization of payload is done via `MqttMessageSerialization` helper class, serialization and deserialization methods requires `JsonSerializerOptions` that is configured during setup of the Mqtt client.
70+
71+
- JsonSerializationOptions
72+
73+
```cs
74+
// Program.cs
75+
builder.Services.AddMqttClient(builder: (provider, options) =>
76+
{
77+
options.JsonSerializerOptions.Converters.Add(new MyCustomConverter()) // here you add your custom converter
78+
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; // change how references are handled
79+
//etc..
80+
})
81+
//.. other code
82+
```
83+
6884
if you want to implement your custom service based on the `IManagedMqttClient` the recommended way is as follow :
6985
- In your `program.cs` add the following:
7086

@@ -244,3 +260,6 @@ route.MapGet("/my-topic/{id:int}", ctx => {
244260
});
245261
...
246262
```
263+
264+
265+

0 commit comments

Comments
 (0)