I have a tree structure of nodes, where each node has 0-n subnodes. Each node has a set of properties. I would like to expose that structure in a webservice.
let's say my route template for accessing a node is
[HttpGet("nodes/{nodeId:int}")]
and for accessing it's subnodes, its
[HttpGet("nodes/{nodeId:int}/nodes")]
What i'd love is a way to specify a route template that matches the {nodeId:int}/nodes
part 1-n times, and maps to an action parameter of type IEnumerable<int>
I know i can use a catch-all template ({**path}
) and do the parsing myself, but i'm wondering if there's a better way.
I'm open to other suggestions on how to expose this structure in a RESTful way. Important: those nodeIds
are not unique across the service. Just within the parent.
Bonus question: Is there something in the OpenAPI standard that handles paths like that? After all, i'd like to describe the endpoint in a way that might even be understood by tooling, client generation, etc.
The only solution that comes to my mind is to write custom model binder. And I don't think ASP.NET supports dynamic paths, like want, to match any number pairs "{nodeId}/nodes".
Keeping all that in mind, I would write custom model binder, that would also validate the path. Here's implementation:
public class NodePathBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue("nodePath").FirstValue;
if (string.IsNullOrEmpty(value))
{
bindingContext.Result = ModelBindingResult.Success(new List<int>());
return Task.CompletedTask;
}
var segments = value.Split('/', StringSplitOptions.RemoveEmptyEntries);
var nodesPartsValid = segments
.Where((s, i) => i % 2 == 1) // validate the "nodes" parts
.All(x => x == "nodes");
if(!nodesPartsValid || !TryGetIds(segments, out var ids))
{
throw new InvalidOperationException();
}
bindingContext.Result = ModelBindingResult.Success(ids);
return Task.CompletedTask;
}
private static bool TryGetIds(string[] segments, out List<int> ids)
{
try
{
ids = segments
.Where((s, i) => i % 2 == 0) // filter out the "nodes"
.Select(int.Parse)
.ToList();
return true;
}
catch
{
ids = new List<int>(0);
return false;
}
}
}
Then you would need to define attribute to use in endpoint definitions:
public class FromNodePathAttribute : ModelBinderAttribute
{
public FromNodePathAttribute() : base(typeof(NodePathBinder)) { }
}
And then you could use it in an endpoint, with following path definition:
[HttpGet("/nodetest/{*nodePath}")]
public IActionResult MyAction([FromNodePath] List<int> nodePath)
{
return Ok(nodePath);
}