Preface
I recently was assigned a project for college, were using ASP.NET Core 6 Web API, and Entity Framework. Due to the subject at hand the objective is to implement an API without taking any consideration around the structure and routing of it.
So this is a warning: I did not choose the routing nor can I change it, even if slightly.
The actual problem
I have 3 model classes, let's call them Application
, Module
and Data
. A single application has various child modules, while each module has a series of data children.
The issue is that when I want to create a Module
or a Data
model, the teachers decided to give it a twist and we're forced to take data from the body and the URL.
So that everyone is on the same page ill give an example of how to create a Module
.
The url goes like this
POST: https://ip:port/api/OurProjectName/{application}/
POSTBODY:
<?xml version="1.0"?>
<Module><!--here on the root node i can be a bit malleable and name it whatever i want so no issues with DTO's-->
<id>99999</id>
<name>some name</name>
<creation_dt>{the current timestamp}</creation_dt>
</Module>
If the implementation was correct what it would do is go to the controller grab the application string and the Module
/ModuleDTO
, fetch an Application
model from the database where the application string is equal to the field name.
Then inside that function due to our choice of using EF, we would just grab the module sent in the body(or create one in case of a DTO) and associate the respective parent sent in the route parameters.
My failed attempts
Although I talked about modules, the behaviour of data is nearly the same as modules except the teachers decided that for some reason with data you can only create and delete, and you can't list, update, etc... When it comes to posting instead of
POST: https://ip:port/api/OurProjectName/{application}/
I have to do something like
POST: https://ip:port/api/OurProjectName/{application}/{module}
while application this time only serves for checking past errors due to the way the database was structured.
I firstly attempted to do something like
[HttpPost("{application}/{module}")]
[Produces("application/xml")]
[Consumes("application/xml")]public IActionResult Post(string application, string module, [FromBody] Data data)
{
var mod = _context.Modules.SingleOrDefault(m => m.name == module);
if (mod == null)
{
return NotFound();
}
if (mod.parent.name != application)
{
//... this is irrelevant for now
}
data.parent = mod;
// TODO: dereference here... fix later
mod.datas.Add(data);
_context.Add(data);
_context.SaveChanges();
return Ok(data);
}
But it returned the usual error of 415, although this try was mostly hoping that [FromBody]
wouldn't complain at the routing as I've had good results with put
(although in that situation it kinda follows convention instead of completely breaking it) and to top it off since Data has a navigation field, swagger will iterate the objects until no parents are found so the xml object in the body looked... well like a configuration file.
I then tried to use decorations around the two strings [FromRoute]
, but it still wouldn't budge, and got an "unsupported media format".
Then I tried to do some custom manual routing (as in no decorations)
here's my startup file that I created with only that purpose in mind (since I otherwise would expect the Program.cs
file to get cluttered which is a thing I hate)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}
var option = new RewriteOptions();
option.AddRedirect("^$", "swagger");
app.UseRewriter(option);
app.UseCors("AllowAll");
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
endpoints.MapControllerRoute(
name: "postDataCustom",
pattern: "api/OurProjectName/{application}/{module}", new {controller = "DataController", action = "Post"});
});
}
I removed the [HttpPost(etc)]
and expected to hopefully fix it, but when I checked swagger not only was it missing but the endpoint didn't even exist (I tested with Postman).
I commented it out (the part where I manually set a ControllerRoute
) and found some other posts around the subject.
And I saw that I could actually do some kind of dto and put the decorations in each field, this was game breaking as I knew then I would be able to just place a single variable inside the controller function and avoid [FromData]
to go insane and parse properly the stuff.
public IActionResult Post(DataDTO data)
{
var mod = _context.Modules.SingleOrDefault(m => m.name == data.module);
if (mod == null)
{
return NotFound();
}
Console.WriteLine(mod.name);
// ...(if it wrote to console I could progress)
return Ok();
}
public class DataDTO
{
[FromRoute]
public string application { get; set; }
[FromRoute]
public string module { get; set; }
[FromBody]
public Data data { get; set; }
public DataDTO(string application, string module, Data data)
{
this.application = application;
this.module = module;
this.data = data;
}
}
It still gave me the error code, this time since I was a bit more skeptical of such a failure, I made a much simpler DTO and routing just to check if I could at least get a 200 code and I did it, the problem is [FromRoute]
is pretty much "ignored"
[HttpPost("{module}")]
public IActionResult Post(testeDTO data)
{
Console.WriteLine(data.module);
Console.WriteLine(data.bb);
return Ok();
}
public class testeDTO
{
[FromRoute]
public string? module { get; set; }
[FromBody]
public string bb { get; set; }
public testeDTO(string bb)
{
this.bb = bb;
}
}
Although the route is there and works fine it never assigns the value to module, it seems like whatever is on the body overwrites it (I tried to get around not having duplicate fields by adding the possibility of module being null so I wouldn't be forced to send a module in the body, but no dice, I can't decouple both behaviors).
At this point I'm kinda out of ideas, it's even more frustrating when old documentation gets in the way with search results.
I've also considered using x-www-urlencode
, although I'm not sure if it's an option I can take as again the requirements are very restrictive.
TL;DR I can't get a post with parameters in the route to hit successfully the action Post...
Update as of 26/12/2022: I was playing around the code and weirdly this version "works" i tested it with swagger and it seems to not produce any error, i even checked git to see if added or removed some kind of config or something of the kind but since the last commit(which was uncharacteristic of me as it was a "backup" of sorts) these were the only things that changed
[HttpPost("{module}")]
public IActionResult Post([FromRoute]string module, [FromBody]testeDTO data){
Console.WriteLine(module);
Console.WriteLine(data.bb);
return Ok();
}
...
public class testeDTO{
[FromBody]
public string bb{get;set;}
public testeDTO(string bb){
this.bb = bb;
}
}
I will try to now play around the code to see if i can get to the solution