diff --git a/README.md b/README.md
index af1dd29..f8ad201 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ service or service instance.
- Detects new and/or removed network interfaces
- Supports multicasting on multiple network interfaces
- Supports reverse address mapping
+- Supports service subtypes (features)
- Handles legacy unicast queries, see #61
## Getting started
diff --git a/doc/articles/subtype.md b/doc/articles/subtype.md
new file mode 100644
index 0000000..3b4d63b
--- /dev/null
+++ b/doc/articles/subtype.md
@@ -0,0 +1,38 @@
+# Subtypes
+
+Subtypes are used to define features implemented by a service instance. See
+[RFC 6763 - 7.1 Selective Instance Enumeration (Subtypes)](https://tools.ietf.org/html/rfc6763#section-7.1) for the details.
+
+
+## Finding service instances
+
+[QueryServiceInstances](xref:Makaretu.Dns.ServiceDiscovery.QueryServiceInstances*) is used to find the
+all the instances of a service with a specific feature.
+The [ServiceInstanceDiscovered](xref:Makaretu.Dns.ServiceDiscovery.ServiceInstanceDiscovered) event is raised
+each time a service instance is discovered.
+
+```csharp
+using Makaretu.Dns;
+
+var sd = new ServiceDiscovery();
+sd.ServiceInstanceDiscovered += (s, e) =>
+{
+ Console.WriteLine($"service instance '{e.ServiceInstanceName}'");
+};
+sd.QueryServiceInstances("_myservice", "apiv2");
+```
+
+## Advertising
+
+Create a [ServiceProfile](xref:Makaretu.Dns.ServiceProfile) with a feature
+and then [Advertise](xref:Makaretu.Dns.ServiceDiscovery.Advertise*) it. Any queries for the service or
+service instance will be answered with information from the profile.
+
+```csharp
+using Makaretu.Dns;
+
+var profile = new ServiceProfile("x", "_myservice._udp", 1024);
+profile.Subtypes.Add("apiv2");
+var sd = new ServiceDiscovery();
+sd.Advertise(profile);
+```
\ No newline at end of file
diff --git a/doc/articles/toc.yml b/doc/articles/toc.yml
index 2ba6bb2..cfa5a07 100644
--- a/doc/articles/toc.yml
+++ b/doc/articles/toc.yml
@@ -4,5 +4,8 @@
href: ms.md
- name: Service Discovery
href: sd.md
+ items:
+ - name: Subtypes
+ href: subtype.md
- name: Class Reference
href: ../api/Makaretu.Dns.yml
diff --git a/src/ServiceDiscovery.cs b/src/ServiceDiscovery.cs
index 2347102..1a88698 100644
--- a/src/ServiceDiscovery.cs
+++ b/src/ServiceDiscovery.cs
@@ -167,6 +167,24 @@ public void QueryServiceInstances(string service)
Mdns.SendQuery(service + ".local", type: DnsType.PTR);
}
+ ///
+ /// Asks instances of the specified service with the subtype to send details.
+ ///
+ ///
+ /// The service name to query. Typically of the form "_service._tcp".
+ ///
+ ///
+ /// The feature that is needed.
+ ///
+ ///
+ /// When an answer is received the event is raised.
+ ///
+ ///
+ public void QueryServiceInstances(string service, string subtype)
+ {
+ Mdns.SendQuery($"{subtype}._sub.{service}.local", type: DnsType.PTR);
+ }
+
///
/// Asks instances of the specified service to send details.
/// accepts unicast and/or broadcast answers.
@@ -209,6 +227,16 @@ public void Advertise(ServiceProfile service)
new PTRRecord { Name = service.QualifiedServiceName, DomainName = service.FullyQualifiedName },
authoritative: true);
+ foreach (var subtype in service.Subtypes)
+ {
+ var ptr = new PTRRecord
+ {
+ Name = $"{subtype}._sub.{service.QualifiedServiceName}",
+ DomainName = service.FullyQualifiedName
+ };
+ catalog.Add(ptr, authoritative: true);
+ }
+
foreach (var r in service.Resources)
{
catalog.Add(r, authoritative: true);
diff --git a/src/ServiceProfile.cs b/src/ServiceProfile.cs
index dc8864f..27f1e2e 100644
--- a/src/ServiceProfile.cs
+++ b/src/ServiceProfile.cs
@@ -154,6 +154,15 @@ public ServiceProfile(string instanceName, string serviceName, ushort port, IEnu
///
public List Resources { get; set; } = new List();
+ ///
+ /// A list of service features implemented by the service instance.
+ ///
+ ///
+ /// The default is an empty list.
+ ///
+ ///
+ public List Subtypes { get; set; } = new List();
+
///
/// Add a property of the service to the .
///
diff --git a/test/ServiceDiscoveryTest.cs b/test/ServiceDiscoveryTest.cs
index d854e7f..8437bb5 100644
--- a/test/ServiceDiscoveryTest.cs
+++ b/test/ServiceDiscoveryTest.cs
@@ -121,6 +121,39 @@ public void Advertises_ServiceInstance_Address()
}
}
+ [TestMethod]
+ public void Advertises_ServiceInstance_Subtype()
+ {
+ var service = new ServiceProfile("x2", "_sdtest-1._udp", 1024, new[] { IPAddress.Loopback });
+ service.Subtypes.Add("_example");
+ var done = new ManualResetEvent(false);
+
+ var mdns = new MulticastService();
+ mdns.NetworkInterfaceDiscovered += (s, e) =>
+ mdns.SendQuery("_example._sub._sdtest-1._udp.local", DnsClass.IN, DnsType.PTR);
+ mdns.AnswerReceived += (s, e) =>
+ {
+ var msg = e.Message;
+ if (msg.Answers.OfType().Any(p => p.DomainName == service.FullyQualifiedName))
+ {
+ done.Set();
+ }
+ };
+ try
+ {
+ using (var sd = new ServiceDiscovery(mdns))
+ {
+ sd.Advertise(service);
+ mdns.Start();
+ Assert.IsTrue(done.WaitOne(TimeSpan.FromSeconds(1)), "query timeout");
+ }
+ }
+ finally
+ {
+ mdns.Stop();
+ }
+ }
+
[TestMethod]
public void Discover_AllServices()
{
@@ -213,6 +246,43 @@ public void Discover_ServiceInstance()
}
}
+ [TestMethod]
+ public void Discover_ServiceInstance_with_Subtype()
+ {
+ var service1 = new ServiceProfile("x", "_sdtest-2._udp", 1024);
+ var service2 = new ServiceProfile("y", "_sdtest-2._udp", 1024);
+ service2.Subtypes.Add("apiv2");
+ var done = new ManualResetEvent(false);
+ var mdns = new MulticastService();
+ var sd = new ServiceDiscovery(mdns);
+
+ mdns.NetworkInterfaceDiscovered += (s, e) =>
+ {
+ sd.QueryServiceInstances("_sdtest-2._udp", "apiv2");
+ };
+
+ sd.ServiceInstanceDiscovered += (s, e) =>
+ {
+ if (e.ServiceInstanceName == service2.FullyQualifiedName)
+ {
+ Assert.IsNotNull(e.Message);
+ done.Set();
+ }
+ };
+ try
+ {
+ sd.Advertise(service1);
+ sd.Advertise(service2);
+ mdns.Start();
+ Assert.IsTrue(done.WaitOne(TimeSpan.FromSeconds(1)), "instance not found");
+ }
+ finally
+ {
+ sd.Dispose();
+ mdns.Stop();
+ }
+ }
+
[TestMethod]
public void Discover_ServiceInstance_Unicast()
{
diff --git a/test/ServiceProfileTest.cs b/test/ServiceProfileTest.cs
index 2ddeefe..4f0f95e 100644
--- a/test/ServiceProfileTest.cs
+++ b/test/ServiceProfileTest.cs
@@ -95,5 +95,12 @@ public void TTLs()
Assert.AreEqual(TimeSpan.FromMinutes(75), service.Resources.OfType().First().TTL);
Assert.AreEqual(TimeSpan.FromSeconds(120), service.Resources.OfType().First().TTL);
}
+
+ [TestMethod]
+ public void Subtypes()
+ {
+ var service = new ServiceProfile("x", "_sdtest._udp", 1024);
+ Assert.AreEqual(0, service.Subtypes.Count);
+ }
}
}