.NET Core Web Api enum binding - c#

I often encounter this situation when developing web apps where the client can choose/set an enum value (maybe through an API, not providing a select box like in frontend).
So I need to handle incorrect values. I am using FluentValidation with Swagger for this. I want to display enums as strings (Swagger) so I register the converter:
services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
I have weird findings for enum modelbinding in .NET CORE:
Consider enum { A,B,C } and tested value bindings:
value "a" -> Enum.A -> as expected
value 1 -> Enum.B -> as expected
value 10 -> 10 -> o_0 - is that new behaviour in CORE?
value "x" -> binding error -> as expected
value null -> Enum.A -> as expected
But how would I deal best with an invalid values?
I certainly do not want value 10 or Enum.A for null values.
I could think of two approaches:
i) Handle all invalid values like binding errors
ii) Bind invalid values to a an explicit invalid enum default Enum.Undefined and validate that this value never occurs
i) How would I enforce stricter binding?
ii) How can I force binding invalid values to Enum.Undefined?
UPDATE
I realized that 3) is unchanged behaviour, I just did not came across this issue. I find that very counter intuitive though.
I also realized that I could write my own enum model binder but I do not want to overengineer things.
What I did for now is following:
enum MyEnum { undefined, a,b ,c}
and in FluentValidation validator:
this.RuleFor(x => x.MyEnum ).Must(
x =>
{
if (!Enum.IsDefined(x)) // can be crap!
{
return false;
}
// still can be Undefined
return x != MyEnum.Undefined;
});
This way I handle incorrect int values. Incorrect string values can not be handled that way because they will break at .NET model binding level. I guess I can live with that.
UPDATE 2
After some more thoughts about that issue I came up with following solution:
I just use string over enum in my DTO and therefore I can completely workaround model binding issues and use validations as follows:
this.RuleFor(x => x.Type).Must(
x => Enum.TryParse(typeof(MyEnum), x, out _));

Related

How to discover missing type-maps for mapping enum to enum in AutoMapper?

Ok folks, this is a rather long question, where I´m trying my best to describe the current situation and provide some meaningful context, before I´m comming to my actual question.
TL;DR;
I need a way to identify invalid enum-to-enum mappings, which might cause runtime-issues, as their definitions diverged over the time.
Some Context
So my team and I are maintaining this rather complex set of REST-APIs...complex at least when it comes down to the actual object-graphs involved.
We have to deal with some hundreds of models in total.
To raise structural complexity, the original architectures went with full n-tier-style on an inner API-level.
On top of that, we´re having multiple of such architectured services, which sometimes need calling each other.
This is achieved through either ordinary http-calls here, some messaging there, you get the idea.
For letting an API communicate with another, and to maintain SOA- and/or microservice-principles, every API at least provides a corresponding client-library, which manages communication with it´s representing API, regardless of the actual underlying protocol involved.
Boiling this down, this incorporates at least the following layers per API (top-down)
Client-Layer
API-Layer
Domain-Layer
Persistence-Layer
Additionally, all those layers maintain their own representation of the various models. Often, those are 1:1 representation, just in another namespace. Sometimes there are more significant differences in between these layers. It depends...
To reduce boiler-plate when communicating between these layers, we´re falling back on AutoMapper most of the time (hate it or love it).
The problem:
As we evolve our overall system, we more and more noticed problems when mapping enum-to-enum properties within the various representations of the models.
Sometimes it´s because some dev just forgot to add a new enum-value in one of the layers, sometimes we re-generated an Open-API based generated client, etc., which then leads to out-of-sync definitions of those enums. The primary issue is, that a source enum may have more values then the target enum.
Another issue might occur, when there are slight differences in the naming, e.g. Executer vs. Executor
Let´s say we have this (very very over-simplified) model-representations
public enum Source { A, B, C, D, Executer, A1, B2, C3 } // more values than below
public enum Destination { C, B, X, Y, A, Executor } //fewer values, different ordering, no D, but X, Y and a Typo
class SourceType
{
public Source[] Enums { get; set; }
}
class DestinationType
{
public Destination[] Enums { get; set; }
}
Now let´s say our AutoMapper config looks something like this:
var problematicMapper = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
}).CreateMapper();
So mapping the following model is kind of a jeopardy, semantic-wise (or at least offers some very odd fun while debugging).
var destination = problematicMapper.Map<DestinationType>(new SourceType()
{
Enums = new []
{
Source.A,
Source.B,
Source.C,
Source.D,
Source.Executer,
Source.A1,
Source.B2,
Source.C3
}
});
var mappedValues = destination.Enums.Select(x => x.ToString()).ToArray();
testOutput.WriteLine(string.Join(Environment.NewLine, mappedValues));
/*
Source.A => A <- ✔️ ok
Source.B => b <- ✔️ok
Source.C => c <- ✔️ok
Source.D => Y <- 🤷‍♀️ whoops
Source.Executer => A <- 🧏‍♂️ wait, what?
Source.A1 => Executor <- 🙊 nah
Source.B2 => 6 <- 🙉 wtf?
Source.C3 => 7 <- 🙈 wth?
*/
bare with me, as some situations here are staged and possibly more extreme than found in reality. Just wanted to point out some weird behavior, even with AutoMapper trying to gracefully handle most cases, like the re-orderings or different casings. Currently, we are facing either more values in the source-enum, or slightly differences in naming / typos
Fewer fun can be observed, when this ultimately causes some nasty production-bugs, which also may have more or less serious business-impact - especially when this kind of issue only happens during run-time, rather than test- and/or build-time.
Additionally, the problem is not exclusive to n-tier-ish architectures, but could also be an issue in orthogonal/onion-/clean-ish-architecture styles (wheras in such cases it should be more likely that such value-types would be placed somewhere in the center of the APIs, rather than on every corner / outer-ring /adapter-layer or whatever the current terminology is)
A (temporary) solution
Despite trying to reduce the shear amount of redundancy within the respective layers, or (manually) maintaining explicit enum-values within the definitions itself (which both are valid options, but heck, this is a lot of PITA-work), there is not much left to do while trying to mitigate this kind of issues.
Gladly, there is a nice option available, which levereages mapping enum-to-enum-properties per-name instead of per-value, as well as doing more customization on a very fine-granular level on a per-member-basis.
[AutoMapper.Extensions.EnumMapping] to the rescue!
from the docs:
The package AutoMapper.Extensions.EnumMapping will map all values from Source type to Destination type if both enum types have the same value (or by name or by value)
and
This package adds an extra EnumMapperConfigurationExpressionExtensions.EnableEnumMappingValidation extension method to extend the existing AssertConfigurationIsValid() method to validate also the enum mappings.
To enable and cusomize mappings, one should just need to create the respective type-maps within AutoMapper-configuration:
var mapperConfig = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
config.CreateMap<Source, Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
config.EnableEnumMappingValidation();
});
mapperConfig.AssertConfigurationIsValid();
Which then would validate even enum-to-enum mappings.
The question (finally ^^)
As our team previously did not (need to) configure AutoMapper with maps for every enum-to-enum mapping (as was the case for dynamic-maps in previous-versions of AutoMapper), we´re a bit lost on how to efficiently and deterministically discover every map needed to be configured this way. Especially, as we´re dealing with possibly a couple of dozens of such cases per api (and per layer).
How could we possibly get to the point, where we have validated and adapted our existing code-base, as well as further preventing this kind of dumbery in the first place?
Leverage custom validation to discover missing mappings during test-time
Ok, now this approach leverages a multi-phased analysis, best fitted into an unit-test (which may already be present in your solution(s), nevertheless).
It´s not a golden gun to magically solve all your issues which may be prevalent, but puts you into a very tight dev-loop which should help clean up things.
Period.
The steps involved are
enable validation of your AutoMapper-configuration
use AutoMapper custom-validation to discover missing type maps
add and configure missing type-maps
ensure maps are valid
adapt changes in enums, or mapping logic (whatever best fits)
this can be cumbersome and needs extra attention, depending on the issues discovered by this approach
rinse and repeat
Examples below use xUnit. Use whatever you might have at hands.
0. starting point
We´re starting with your initial AutoMapper-configuration:
var mapperConfig = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
});
1. enable validation of your AutoMapper-Configuration
Somewhere within your test-suit, ensure you are validating your AutoMapper-configuration:
[Fact]
public void MapperConfigurationIsValid() => mapperConfig.AssertConfigurationIsValid();
2. use AutoMapper custom-validation to discover missing type maps
Now modify your AutoMapper-configuration to this:
mapperConfig = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
config.Advanced.Validator(context => {
if (!context.Types.DestinationType.IsEnum) return;
if (!context.Types.SourceType.IsEnum) return;
if (context.TypeMap is not null) return;
var message = $"config.CreateMap<{context.Types.SourceType}, {context.Types.DestinationType}>().ConvertUsingEnumMapping(opt => opt.MapByName());";
throw new AutoMapperConfigurationException(message);
});
config.EnableEnumMappingValidation();
});
This does a couple of things:
look for mappings, that map from an enum to an enum
which have no type-map associated to them (that is, they were "generated" by AutoMapper itself and hence are lacking an explicit CreateMap call)
if (!context.Types.DestinationType.IsEnum) return;
if (!context.Types.SourceType.IsEnum) return;
if (context.TypeMap is not null) return;
Raise an error, which message is the equivalent of the actual call missing to CreateMap
var message = $"config.CreateMap<{context.Types.SourceType}, {context.Types.DestinationType}>().ConvertUsingEnumMapping(opt => opt.MapByName());";
throw new AutoMapperConfigurationException(message);
3. add and configure missing type-maps
Re-running our previous test, which should fail now, should output something like this:
AutoMapper.AutoMapperConfigurationException : config.CreateMap<Sample.AutoMapper.EnumValidation.Source, Sample.AutoMapper.EnumValidation.Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
And boom, there you go. The missing type-map configuration call on a silver-plate.
Now copy that line and place it somewhere suitable withing your AutoMapper-configuration.
For this post I´m just putting it below the existing one:
config.CreateMap<SourceType, DestinationType>();
config.CreateMap<Sample.AutoMapper.EnumValidation.Source, Sample.AutoMapper.EnumValidation.Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
in a real-world scenario, this would be a line for every enum-to-enum mapping that not already has a type-map associated to it within the AutoMapper-configuration. Depending on how you actually configure AutoMapper, this line could need to be slightly adopted to your needs, e.g. for usage in MappingProfiles.
adapt changes in enums
Re-run the test from above, which should fail now, too, as there are incompatible enum-values.
The output should look something like this:
AutoMapper.AutoMapperConfigurationException : Missing enum mapping from Sample.AutoMapper.EnumValidation.Source to Sample.AutoMapper.EnumValidation.Destination based on Name
The following source values are not mapped:
- B
- C
- D
- Executer
- A1
- B2
- C3
There you go, AutoMapper discovered missing or un-mappable enum-values.
note that we lost automatic handling of differences in casing.
What´s to do now heavily depends on your solution and cannot be covered in a SO-post. So take appropriate actions to mitigate.
6. rinse and repeat
Go back to 3. until all issues are solved.
From then on, you should have a saftey-net in place, that should prevent you from falling into that kind of trap in the future.
However, note that mapping per-name instead of per-value might have a negative impact, performance-wise. That should definetley be taken into account when applying this kind of change to your code-base. But with all those inter-layer-mappings present I would guess a possible bottleneck is in another castle, Mario ;)
A full wrapup of the samples shown in this post can be found in this github-repo
I wrote a validator that would check if the Enums match so I don't have to add those Enum Mappings.
var problematicMapperConfiguration = new MapperConfiguration(config =>
{
config.Advanced.Validator(EnumMappingValidator.ValidateNamesMatch());
config.CreateMap<SourceType, DestinationType>();
});
problematicMapperConfiguration.AssertConfigurationIsValid();
With your example it would fail like that:
Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "D".
Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "Executer".
Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "A1".
Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "B2".
Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "C3".
The validator is very simple and looks like this:
public class EnumMappingValidator
{
public static Action<ValidationContext> ValidateNamesMatch()
{
return validationContext =>
{
var sourceEnumType = GetEnumType(validationContext.Types.SourceType);
if (sourceEnumType == null)
return;
var destinationEnumType = GetEnumType(validationContext.Types.DestinationType);
if (destinationEnumType == null) throw new ArgumentException("Unexpected Enum to Non-Enum Map");
var sourceEnumNames = sourceEnumType.GetFields().Select(x => x.Name).ToList();
var destinationEnumNames = destinationEnumType.GetFields().Select(x => x.Name).ToList();
var errors = new List<string>();
foreach (var sourceEnumName in sourceEnumNames)
{
if (destinationEnumNames.All(x => x.ToLower() != sourceEnumName.ToLower()))
errors.Add($"Expected enum {destinationEnumType} to contain enum \"{sourceEnumName}\".");
}
if (errors.Any())
throw new ArgumentException(string.Join(Environment.NewLine, errors));
};
}
private static Type? GetEnumType(Type type)
{
if (type.IsEnum) return type;
var nullableUnderlyingType = Nullable.GetUnderlyingType(type);
if (nullableUnderlyingType?.IsEnum ?? false) return nullableUnderlyingType;
return null;
}
}
Github: https://github.com/matthiaslischka/AutoMapperEnumValidation

Best practice for passing enum params in Web API

I have a RESTful Web API project, and I have 2 different Enum scenarios that I'm unsure of re best practice.
Scenario 1 : Straightforward Enum Param
My API method requires a parameter called ruleType, with valid values being EmailAddress and IPAddress.
My enum within the Web API project looks like this:
public enum RuleType
{
None = 0,
EmailAddress = 1,
IPAddress = 2
}
My question for this scenario is, should I use ?ruleType=EmailAddress in my request to the API (which automatically binds that value to my RuleType property within the API method)? If so, how best to validate that the RuleType param sent, is a valid RuleType Enum value?
Scenario 2 : Multiple Enum Values for a Single Param
My API method has an optional fields param, which is allows you to specify any additional data that should be returned. E.g. &fields=ruleOwner,rule. This would return those 2 extra bits of data in the response.
I have an enum in the Web API project which relates to each possible field that may be requested, and at present, I am splitting the comma separated fields param, then looping through each string representation of that enum, parsing it to the equivalent enum, resulting in a list of Enum values which I can then use within my API to retrieve the relevant data.
This is the Enum:
public enum OptionalField
{
None = 0,
RuleOwner = 1,
Rule = 2,
etc.
}
What would be best practice here? I was looking into bitwise enums, so a single value is sent in the API request which resulted in any combination of fields but didn't know if this would work well with a Web API, or if there's generally a better way to go about this?
The simplest answer is, "It doesn't matter".
If the parameter in your controller method is of the enumeration type
public IHttpActionResult Foo(RuleType ruleType)
In WebAPI, It Just Works - no matter if the client request URL specifies the parmeter value as ?ruleType=1 or ?ruleType=EmailAddress
If the client specifies a value that isn't valid for the enumeration, an exception is thrown (The parameters dictionary contains a null entry for parameter 'ruleType' of non-nullable type 'RuleType' for method 'Foo' ... and the client gets a 400 Bad Request response.
it is a best practice to make the URI "human readable". so i can also understand why you using Enum as a string. But as HristoKolev said you have to write a custom Model Binder.
In fields i think you should not use an combination of enums. because it is difficult to understand. perhaps you can create an combination of enum as enum entry
public enum OptionalField
{
None = 0,
RuleOwner = 1,
Rule = 2,
RuleAndRuleOwner = 3,
etc.
}
For scenario 2 there is built in support in C# for Bitmask operations in Enums using the [Flags] attribute
[Flags]
public enum OptionalField
{
None = 0,
RuleOwner = 1,
Rule = 2,
RuleAdministrator = 4,
RuleEditor = 8,
...etc
}
Which is described in this SO post
As Christian already stated in his answer it's probably not good practice to use this in a REST API but it should work.
Multiple values or not, if you are using an Enum as a string, you have to parse it manually. In .NET the Enums are integers so if you want to send an enum to the default model binder you have to do it like this: ?ruleType=1.
You can write your own model binder that will accept string, but you have to ask yourself why are we doing it? If you want the user to be able to easily identify the url then use strings. If not there is no reason not to use integers. As you said, you can use FlagsAttribute to combine multiple values.
In .Net Core you can add below statement in ConfigureServices() method in StartUp.cs or Program.cs bepending on what version of .NET Core you are using.
services.AddControllers()
.AddJsonOptions(opt=> { opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); });

How to conditionally validate properties using FluentValidation?

I'm trying to implement a rule using FluentValidation where two properties have a dependency on each other. They are Unit and UnitType. Unit is a string, and UnitType is an enum. I want rules where if the Unit is filled out then the UnitType cannot be None (0), and if the UnitType is not None (0) then the Unit must be filled out. Here's the rules I've tried so far to no avail:
this.RuleFor(
p =>
p.Unit).NotEmpty().When(
l =>
(l.UnitType != UnitType.None)).WithMessage("Unit № must be filled out and must be less than 8 characters long when the Unit Type is selected");
this.RuleFor(
p =>
p.UnitType).NotEqual(UnitType.None).When(
l =>
!String.IsNullOrEmpty(l.Unit)).WithMessage("Unit Type must be selected when the Unit № is filled out");
No matter how I tweak the rules, I just keep getting an error that says: 'Unit Type' must not be empty. Since my custom error messages are not showing up, I'm thinking that the rules are being skipped somehow...
I'd appreciate any suggestions on how to get this fixed.
Well, after taking some time away from this and coming back to it, I believe I finally have it resolved. Originally in my post model I had the UnitType property as non-nullable, which is what was triggering the validation even though it's default value was UnitType.None. From what I can tell, MVC, not FluentValidation, sees that it's non-nullable property, ignores the default value and then attempts to bind the property anyway with a null value from the form post. I can kind of understand why it behaves this way, but can't help but think that it shouldn't ignore default values.
So, I changed the property to be nullable and now my two rules work as I expect them to. Still, I'm not overly happy about the way it actually works because I was hoping to use the default auto-generated value further down when mapping with AutoMapper, and now I'll have to make sure to set a default value in the constructor where it's relevant. Realistically a non-issue in the long run, so long as I don't miss setting a default somewhere.

Deserializing a model from an api that contains Enums

We're developing a web application using ASP.Net MVC 5 C#, I'm having difficulties coming up with a nice solution for my problem.
We're accessing an api that has a few enums coming across as text (i.e. "yes", "no", "notfound", "na") on the model itself I'd like to have strongly typed enums that store in the database as an integer (save a little space I think) the issue comes when we're deserializing the response from the api. In the response it will comes across as text (see above) where as the enums will be expecting an integer.
How is this done? I really hate asking questions where I haven't tried anything but my web searches hasn't resulted in anything. If you needed any code please let me know and I'll update the question with the segments needed. As of right now the model has several strongly typed enums and the api is returning strings of the enum values. by the way The enum texts are the same as the return values.
In-case it makes any difference we're also using EF Code First 6
You can use Enum.TryParse to convert the string to its enum value
public enum MyEnum
{
NA
Yes,
No
}
then
MyEnum value = MyEnum.NA;
Enum.TryParse<MyEnum>(theTextValue, out value );

How to solve cast issues in ValidationRule classes' properties?

I need to create a few tests for the user roles in a web application. To minimize the description, one of the tests involves checking if a menu entry is displayed or not for an user.
For this test, I use a table called UserRoles, that looks like this:
sUserName bDoesntHaveMenuX
User1 1
User2 0
User3 1
bDoesntHaveMenuX is of type bit.
I have a class derived from ValidationRule that checks if a certain text is present in a page, based on a XPath expression to locate the node where to look for the text.
The public properties of this class are:
string XPathExpression
string Text
bool FailIfFound
The last one dictates if the rule should fail if the text is found or not found.
In the test I added a datasource for the table mentioned in the beginning, called DS.
For the request I'm interested in I added a new instance of my validation rule class, with the following values:
Text=MenuX
XPathExpression=//div[#id='menu']//td
FailIfFound={{DS.UserRoles.bDoesntHaveMenuX}}
Unfortunately, this doesn't work.
The reason seems to be that the data binding process creates a context variable
DS.UserRoles.bDoesntHaveMenuX has the value "False" or "True". The value is a string, so the binding results in a casting error.
My options, as far as I can think of, are:
Change the validation rule to accept strings for FailIfFound. Not a valid
option, for 2 reasons: it's a hack and the same rule is used in
other places.
Make a new validation rule that will use the above mentioned one,
and implement the FailIfFound as string. I also don't like this, for
the same reason as above. It's a hack.
Make the test coded and do the proper cast before passing the data
to the validation rule. I don't like this one because I prefer to
have the test as coded only if there is no other way.
Which brings me to the question. Is there another way?
Thank you.
So the fundamental issue is that you have no control over how the data-binding treats the 'bit' data type, and it's getting converted to string instead of bool.
The only solution I can think of (which is sadly still a bit of a hack, but not so egregious as changing FailIfFound to string) is to create a WebTestPlugin, and in the PreRequestDataBinding or PreRequest event, convert the value from string to bool. Don't forget to add the plugin to your test(s) (easy mistake I have made).
Then when the validation rule is created it should pick up the nice new bool value and work correctly.
e.g.
string val = e.WebTest.Context["DS.UserRoles.bDoesntHaveMenuX"].ToString();
e.WebTest.Context["DS.UserRoles.bDoesntHaveMenuX"] = (val == "True");
I didn't actually try this... hope it works.
EDIT: round two... a better solution
Change the FailIfFound property to string (in a subclass as you mentioned), so it can work properly with data-binding.
Implement a TypeConverter that provides a dropdown list of valid values for the property in the rule's PropertyGrid (True, False), so in the GUI it looks identical to the rule having FailIfFound as a bool. You can still type your own value into the box when necessary (e.g. for data-binding).
Add the path of the .dll containing the TypeConverter code to your test project's References section.
This is what I have started doing and it is much more satisfying than having to type 'True' or 'False' in the property's edit box.

Categories

Resources