ASPNET Core Options Binding does not populate dictionary key/value pair - asp.net-core

I have the following JSON configuration
"Configurations": {
"KeyA": {
"Ids": []
},
"KeyB": {
"Ids": [1, 2, 3]
},
"KeyC": {
"Ids": [1, 2, 3],
"OptionalData": "asdf"
}
}
This is then read into the following object
public class AppConfiguration
{
public Dictionary<ConfigType, ConfigurationData> Configurations {get; set;} = new Dictionary<ConfigType, ConfigurationData>();
}
public class ConfigurationData
{
public HashSet<int> Ids {get;set;} = new HashSet<int>();
public string OptionalData = "";
}
public Enum ConfigType
{
KeyA = 1,
KeyB = 2,
KeyC = 3
}
I then bind this in ConfigureServices(IServiceCollection services) method using
services.Configure<AppConfiguration>(this.Configuration);
However, I notied that the configuration binding produces my AppConfiguration's dictionary with only the KeyB and KeyC keys, skipping KeyA, because its Ids array is empty. I read up on the behaviour of the configuration binding online, but as far as I saw it should bind the Ids to null, but here it just does not generate a key value pair in the dictionary altogether.
I tried removing the "Ids" property, leaving my config like "KeyA": {}", but this still did not work. The only way I can get it to parse is if I put in some numbers in the array, but this obviously not what I want.
I would like to know if there is anyway I can bind such a key-value pair, where I don't have any Ids in my array. This seems like it should be somehow supported out of the box, but I'm not sure why it's not working and how could I resolve it, without implementing some hacky custom configuration loader/binder.

For anyone who stumbles upon this in the future, I managed to solve this by setting my array to null, instead of an empty array like so.
"Configurations": {
"KeyA": {
"Ids": null
}
}

Related

How to deal with swagger against route overrides?

I'm trying to embed Swagger in my Asp Core (.Net 6) project where there are some cases of route overriding. However, the issue I'm facing can be reproduced even on the following case.
Consider a minimal Asp Core (.Net 6) app. For instance, just like the one used by Swashbuckle as test: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/master/test/WebSites/MinimalApp
Now, consider two controllers in the same app:
[ApiController]
[Route("/api")]
public class MyFallbackController : ControllerBase
{
[HttpGet("values", Order = 1)]
public ActionResult<object> GetValues()
{
return new[] { 1, 2, 3 };
}
}
[ApiController]
[Route("/api")]
public class MyOverrideController : ControllerBase
{
[HttpGet("values", Order = 0)]
public ActionResult<object> GetValues()
{
return new[] { 4, 5, 6 };
}
}
Notice that the routes are exactly the same, but only the first (Order = 0) will be considered.
If I run the app and navigate to:
https://localhost:7263/api/values
the response gives the expected result: [4, 5, 6]
However, when I try to access the Swagger section, it does not work because (apparently) it figures as a collision the controller pair:
An unhandled exception occurred while processing the request.
SwaggerGeneratorException: Conflicting method/path combination "GET
api/values" for actions -
WebApplication2.MyFallbackController.GetValues
(WebApplication2),WebApplication2.MyOverrideController.GetValues
(WebApplication2). Actions require a unique method/path combination
for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a
workaround
Is there any way to get rid of that problem?
Found it.
The trick is in the SwaggerGen configuration, as the exception message suggests, by the way.
using Microsoft.AspNetCore.Mvc.ApiExplorer;
services.AddSwaggerGen(c =>
{
c.ResolveConflictingActions(apiDescriptions =>
{
int best_order = int.MaxValue;
ApiDescription? best_descr = null;
foreach (var curr_descr in apiDescriptions)
{
int curr_order = curr_descr.ActionDescriptor.AttributeRouteInfo?.Order ?? 0;
if (curr_order < best_order)
{
best_descr = curr_descr;
}
}
return best_descr;
});
});
Basically, the above function selects only the ApiDescription with the lowest order among the duplicates.
That is my naive yet effective solution. For instance, I don't know if the input collection is given already sorted by order. In that case, the code could be even simpler.

How to accept an arbitary JSON object?

I've got a Jax-rs endpoint that accepts JSON analogous to:
{
"a": 1,
"b": "some value",
"c": { <-some-arbitary-json-object-> }
}
In my DTO, a and b are no issue. What do I do with c? I need only to serialize it again (or, indeed, just read it as a String), I don't need to process it in any way. I do need to do things with a and b, so I can't just treat the entire body as a String.
What data type do I need to give it so that jax-rs/jersey can deserialize it?
I can't help but feel I'm missing something obvious.
I worked out one way, but I feel I'm probably re-inventing the wheel. I defined a custom deserializer to read in the arbitary JSON then serialise it again:
public class JsonAsStringDeserializer extends JsonDeserializer<String> {
private final ObjectMapper mapper = new ObjectMapper();
#Override
public String deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
TreeNode node = mapper.readTree(p);
return mapper.writeValueAsString(node);
}
}
And in the model POJO:
#JsonDeserialize(using = JsonAsStringDeserializer.class)
private String c = null;

How to change JSON returned by query using Helidon 2.0.0-M-2

I'm using Helidon 2.0.0-M2.
When I run the query below I get back a list of JSON objects.
dbClient.execute(exec -> exec.createNamedQuery("select-dsitem-by-id")
.addParam("userId", dataItemId)
.execute())
.thenAccept(response::send)
.exceptionally(throwable -> sendError(throwable, response));
Returned list
[
{
"data": "qwerty",
"user_id": "12345"
},
{
"data": "qwerty123",
"user_id": "22345"
}
]
The attribute names seem to be taken directly from the database column name. e.g. one attribute name returned is "user_id". However, I want it to be "userId". I also want to create a parent wrapper for this list like:
{
"userList": [
{
"data": "qwerty",
"user_id": "12345"
},
{
"data": "qwerty123",
"user_id": "22345"
}
]
}
What is the best way to do this with the dbclient?
Thanks
Simple approach:
Change your SQL statement to return the correct name, such as:
SELECT data, user_id as userId FROM mytable
Complicated approach:
We are working on a better support to map to a JSON stream.
Currently there is only one (a bit complicated) way to achieve this:
You can create a custom mapper from a DbRow to JsonObject. This mapper needs to be a general one (it must work for any DbRow of any query).
The built-in mapper uses metadata provided on the columns. I have prepared a simple example (that just expects to have a single type of statements):
class DbRecordMapperProvider implements DbMapperProvider {
private static final DbMapper<JsonObject> MAPPER = new DbRecordMapper();
#SuppressWarnings("unchecked")
#Override
public <T> Optional<DbMapper<T>> mapper(Class<T> aClass) {
if (JsonObject.class.equals(aClass)) {
return Optional.of((DbMapper<T>)MAPPER);
}
return Optional.empty();
}
}
class DbRecordMapper implements DbMapper<JsonObject> {
#Override
public JsonObject read(DbRow dbRow) {
return Json.createObjectBuilder()
.add("name", dbRow.column("FIRSTPART").as(String.class))
.add("message", dbRow.column("SECONDPART").as(String.class))
.build();
}
#Override
public Map<String, ?> toNamedParameters(JsonObject dbRecord) {
return dbRecord;
}
#Override
public List<?> toIndexedParameters(JsonObject dbRecord) {
throw new IllegalStateException("Cannot convert json object to indexed parameters");
}
}
The important method is public JsonObject read(DbRow dbRow).
Once you have such a DbMapperProvider, you register it with the DbClient:
dbClient = DbClient.builder()
.config(config.get("db"))
.mapperProvider(new DbRecordMapperProvider())
.build();

Stop all further validation if multiple validation for first field fails

I am trying to write the rules for validating field for the validation.
At first i want to check if Location Id is empty, null or if that exist in the database, and then only processed further for other validation.
But, with the code i used, only check if empty, it does not stop if Location Id does not exist in the database.
Register Model
public class RegisterModel
{
public long LocationId {get;set;}
public long FirstName {get;set;}
public long LastName {get;set;}
//...other property to be added
}
JSON i am passing to API:
{
"FirstName": "John",
"LocationId": "1234534",
}
The location id does not exist in the database: But i am getting the response as:
{
"Errors": [
{
"FieldName": "LastName",
"Message": "'Last Name' must not be empty."
},
{
"FieldName": "LocationId",
"Message": "Invalid request."
}
]
}
Validation rule i am using:
public class RegisterModelValidator : AbstractValidator<RegisterModel>
{
private readonly DbContext _context;
public RegisterModelValidator(DbContext context)
{
this._context = context;
this.CascadeMode = CascadeMode.StopOnFirstFailure;
RuleFor(req => req.LocationId)
.NotEmpty().WithMessage("param1 is missing.")
.Must(IsValidRequest).WithMessage("Invalid request.");
When(x => x.LocationId != null, () => {
RuleFor(x => x.FirstName).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEmpty();
RuleFor(x => x.LastName).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEmpty();
});
}
private bool IsValidRequest(string req)
{
var locationId = long.TryParse(req, out long result) ? result : 0;
return _context.Locations.Any(x => x.LocationExtId == locationId);
}
private bool BeAValidDate(string value)
{
DateTime date;
return DateTime.TryParse(value, out date);
}
}
In my condition what i want is if the location id is missing or does not exist in the database, the validation should stop immediately, it should not check for the other field.
If you look at the docs here, its mentioned as
Setting the cascade mode only applies to validators within the same RuleFor chain. Changing the cascade mode does not affect separate calls to RuleFor. If you want prevent one rule from running if a different rule fails, you should instead use Dependent Rules (below)
So can you try it like this using Dependent Rules
RuleFor(x => x.FirstName).NotNull().NotEmpty()
.DependentRules(d => d.RuleFor(req => req.LocationId)
.NotEmpty().WithMessage("param1 is missing.")
.Must(IsValidRequest).WithMessage("Invalid request."));
But i see if we use it like this, i think it should be repeated for multiple properties.
or a better option for you is using PreValidate since
At first i want to check if Location Id is empty, null or if that exist in the database
Prevalidate a property

Is there a way to escape colon in appsetting.json dictionary key in aspnetcore configuration?

I have this provider dictionary in appsetting.json
"AppSettings": {
"Providers": {
"http://localhost:5001": "Provider1",
"http://localhost:5002": "Provider2"
},
"ArrayWorks": [
"http://localhost:5001",
"http://localhost:5002"
],
"SoDoesColonInDictionaryValue": {
"Provider1": "http://localhost:5001",
"Provider2": "http://localhost:5002"
}
}
And the following throw exception because there's colon in the dictionary key.
Configuration.GetSection("AppSettings").Get<AppSettings>()
However, colon works fine as dictionary value, or array, just not dictionary key.
I read colon has special meaning in config, but there seems no way to escape. Why?
Edit:
public class AppSettings
{
public string ApplicationName { get; set; }
public IDictionary<string, string> Providers { get; set; }
}
When debugging Configuration.GetSection("AppSettings"), you get this
Key AppSettings:Providers:http://localhost:5000
Value Provider1
It was intended to be something like this
Key AppSettings:Providers:http_//localhost_5000
But there seems no way to control how Configuration treat the :::
Edit:
According to aspnet/Configuration#792
Colons are reserved for special meaning in the keys, so they shouldn't
be used as part of normal key values.
This isn't supported and issue was closed.
Not yet, Until now there is no escape colon character, Accourding to Microsoft Asp.net repository on github, but there is an open issue with #782 on the github repository which move it to this backlog
As a workaround you can reverse the key with the value in appsetting:AppSettings and correct it in code like the below:
"AppSettings": {
"Providers": {
"Provider1":"http://localhost:5001",
"Provider2":"http://localhost:5002"
},
"ArrayWorks": [
"http://localhost:5001",
"http://localhost:5002"
],
"SoDoesColonInDictionaryValue": {
"Provider1": "http://localhost:5001",
"Provider2": "http://localhost:5002"
}
}
And in code make sure to reverse dictionary key and value as the below
var result = _configuration.GetSection("AppSettings:Providers")
.GetChildren().ToDictionary(i=>i.Value,i=>i.Key);
// result["http://localhost:5001"] return Provider1
// result["http://localhost:5002"] return Provider2