When trying to get an answer using hotchocolate, in fields with enum type the answer is in the form of a string, and I need an enum value
[UseProjection]
public IQueryable<Cart> GetActualCart([Service] ApplicationDbContext context, ClaimsPrincipal claimsPrincipal, int pageNumber = 1, int pageSize = 10)
{
var userId = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);
return context.Carts
.Where(f => f.CourierId == new Guid(userId))
.FromPage(pageNumber, pageSize);
}
public class Cart : AuditableBaseEntity<Guid>
{
public Guid CourierId { get; set; }
public CartStatus Status { get; set; } = CartStatus.Building;
public virtual ICollection<CartItem> Items { get; set; } = new HashSet<CartItem>();
}
public enum CartStatus
{
Building = 1,
WarehouseDelivery = 2,
WarehouseRefund = 3,
CartRefund = 4,
}
RESPONSE:
"data": {
"actualCart": [
{
"courierId": "efb60c9e-c6fe-4479-bd93-82fb23ad63b5",
"status": "BUILDING"
},
{
"courierId": "efb60c9e-c6fe-4479-bd93-82fb23ad63b5",
"status": "WAREHOUSE_DELIVERY"
}
]
A GraphQL enum will always yield the name as a response. Infact the GraphQL enum element name cannot start with a number.
https://spec.graphql.org/October2021/#sec-Enum-Value
The name specification is here:
https://spec.graphql.org/October2021/#Name
If it is acceptable that the Cart.status field is of type Int instead of being an enum type, you can bind the CartStatus to the IntType and use a converter:
public void ConfigureServices(IServiceCollection services)
{
services
.AddGraphQLServer()
.BindRuntimeType<CartStatus, IntType>()
.AddTypeConverter<CartStatus, int>(source => (int) source);
}
First of all, consumers of your API should not care for the numeric enum value. If they do it's a clear sign that your abstraction is leaking and your consumers are depending on an implementation detail of your API.
That being said, as Michael already mentioned there is no spec compliant way to return a number literal as an enum value. You could work around this, but then your server would no longer be spec compliant, which will introduce a whole new suit of issues for you.
The simplest way to solve your problem would be to just swap your enum for the Int scalar:
builder.Services
.AddGraphQLServer()
// Each occurence of the CartStatus should be represented as Int
.BindRuntimeType<CartStatus, IntType>()
// How to convert a CartStatus to the integer value (output)
.AddTypeConverter<CartStatus, int>(source => (int)source)
// How to convert an int to the CartStatus enum value (input)
.AddTypeConverter<int, CartStatus>(source => (CartStatus)source);
If you REALLY need the number value, but you also want to keep the enum for discoverability's sake, you could create a wrapper type that returns both the enum value and the number value. Assuming your enum is named Status you can create a wrapper object like the following:
builder.Services
.AddGraphQLServer()
.BindRuntimeType<Status, StatusObjectType>();
public class StatusObjectType : ObjectType
{
protected override void Configure(IObjectTypeDescriptor descriptor)
{
descriptor.Name("StatusObject");
descriptor.Field("value")
.Type<EnumType<Status>>()
.Resolve(context => context.Parent<Status>());
descriptor.Field("code")
.Resolve(context => (int)context.Parent<Status>());
}
}
For every resolver that returns the Status enum it will automatically map to this wrapper object.
A consumer would then be able to query your enum field like this:
Related
For all my other enum classes swagger shows the string definition but for one enum class that I use in my 'ExceptionMiddleware' class it shows the numeric value. But in the swagger documentation example it shows the string value..
My enum class :
public enum ErrorCode
{
Undefined = -1,
None = 0,
ContractNotFound = 1000
}
One of my other enum classes that doesn't have this "problem" :
public enum ContractStatus
{
Undefined = 0,
Created = 1,
Valid = 2,
Invalid = 3
}
A result when the contract is not found :
I also have to add '[JsonPropertyName("errorCode")]' so the properties start with a lowercase letter. For all my other models this is not needed...
The class :
public class ExceptionResponse
{
[JsonPropertyName("errorCode")]
public ErrorCode ErrorCode { get; set; }
[JsonPropertyName("errorCodeLabel")]
public string ErrorCodeLabel { get; set; }
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
}
Configuration in 'Program.cs' :
o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
If I remove this all enum's show numeric values instead of string values.
How I build the 'ExceptionResponse' model in my 'ExceptionMiddleware' class :
var exceptionResponse = new ExceptionResponse()
{
ErrorCode = ErrorCode.Undefined,
ErrorCodeLabel = ErrorCode.Undefined.ToString(),
ErrorMessage = "A random message."
};
And if there is an error :
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(exceptionResponse));
Because you are manually serializing, you are bypassing the registered middleware that contains your converter. You need to pass an options instance to your serialize call that includes the converter.
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(exceptionResponse, options));
It is considered a best practice to cache your serializer options as a static field so that you are not re-creating them each call to Serialize, since this has severe performance repercussions in older versions of System.Text.Json.
public class ContainingClass
{
private static readonly JsonSerializerOptions s_options = new()
{
Converters =
{
new JsonStringEnumConverter()
}
}
}
You can put this attribute above your Enum declaration. Something like this
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ErrorCode
{
Undefined = -1,
None = 0,
ContractNotFound = 1000
}
Note that this is for .net core 3 and above. More info for the built-in converter is here: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonstringenumconverter?view=net-7.0
I'm migrating an old API to .net core web api and one of the responses includes the same value twice, so I'm using the native Json library of .NET 5 and I'm trying to get the same value twice in the JSON response, 'Id' and 'id'
{
...
"Id": "10",
"id": "10"
...
}
In my Startup, ConfigurationServices I configured the Json Option like this:
services.AddControllers().AddJsonOptions(options =>
{ options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; });
My action method
[HttpGet]
public async Task<ActionResult<IEnumerable<object>>> GetContacts(string projectID)
{
Project project = _context.Projects.Where(a => a.Name == projectID)
.FirstOrDefault();
var contacts = await _context.Contacts.Where(a => a.ProjectId == project.Id)
.Select(o => new { id = o.Id, ID = o.Id}).ToListAsync();
return contacts;
}
While serializing, I am getting the "The JSON property name for collides with another property."
I think I'm missing something, and I'm stuck in this.
According to docs for PropertyNameCaseInsensitive:
Gets or sets a value that determines whether a property's name uses a case-insensitive comparison during deserialization.
So this flag is not about serialization and API output formatting. In the example code it is set to true. Hence, during deserialization a JSON property name should be matched with a single property of a target class in a case-insensitive manner. However, there is a clash - there are two candidate properties - Id and id. So it does not make sense.
Internally it's implemented as a case-insensitive dictionary for property lookup (decompiled .Net 5 by Rider):
public JsonClassInfo(Type type, JsonSerializerOptions options)
{
// ...
Dictionary<string, JsonPropertyInfo> cache = new Dictionary<string, JsonPropertyInfo>(
Options.PropertyNameCaseInsensitive
? StringComparer.OrdinalIgnoreCase
: StringComparer.Ordinal);
// ...
}
So the solution is to set PropertyNameCaseInsensitive to false and use PropertyNamingPolicy = JsonNamingPolicy.CamelCase (which is the default value and is omitted below):
public class SomeObject
{
[JsonPropertyName("Id")]
public int Id { get; set; }
public int id { get; set; }
public string SomeString { get; set; }
}
Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.PropertyNameCaseInsensitive = false);
// ...
}
This should give:
{
"Id": 2,
"id": 3,
"someString": "..."
}
In our system, we have this enum and you can see the value starts with 1, not 0.
public enum EndType
{
[Description("DAYEND")]
DayEnd = 1,
[Description("MONTHEND")]
MonthEnd = 2
}
Then we have a request object that exposed through a web api and you can see that the PerformanceEndType doesn't have a default value.
public class AnnualisedPerformanceRequest
{
public EndType PerformanceEndType { get; set; };
public bool UseDateRestrictMethod { get; set; } = false;
}
Then when people post this JSON request to our web API:
{
"UseDateRestrictMethod": true
}
The PerformanceEndType property actually has a value 0, which is not in the enum values we defined.
Of course, we can put a default value on this property for this request.
However, as we have a fair amount of enum properties in different request objects, is it possible to have some JSON setting or something, so when we call JsonConvert.DeserializeObject(request), it can work out that for this enum, the default value is 1 not 0?
JsonConvert.DeserializeObject<EndType>(request);
OR
JsonConvert.DeserializeObject<AnnualisedPerformanceRequest>(request);
I have a list of Order objects. Order has the properties: int Id, decimal Price, string OrderNumber, string ShipperState, DateTime TimeStamp;
I know which columns I want to transform (Price, TimeStamp) and I want to keep the other columns without needing to specify them.
This example is transforming specified columns but I still need to include the non-transformed columns.
var myList = model.Orders.Select(x => new
{
x.Id,
x.OrderNumber,
// decimal to string
Price = x.Price.ToString("C", new CultureInfo("en-US")),
x.ShipperState,
// DateTime to string
TimeStamp = x.TimeStamp.ToString("MM/dd/yyyy H:mm")
}
If I were to add a column string ShipperCity to the Order class, I would like myList to also have that property without having to go back and update the projection.
An ideal answer would not rely on external libraries, reflection and only be a line or two.
If you do not want to modify the model class as #David suggested you can write extension methods for it like this:
public static class OrderExtensions
{
public static string GetFormattedPrice(this Order order)
=> order.Price.ToString("C", new CultureInfo("en-US"));
public static string GetFormattedTimestamp(this Order order)
=> order.Timestamp.ToString("MM/dd/yyyy H:mm");
}
UPDATE #1
The effect of this alternative is that whereever you wanted to use the transformed order.Price and order.Timestamp there you have to use order.GetFormattedPrice() and order.GetFormattedTimestamp() respectively.
In the question it was not specified that where the data come from and what type of application the data is used in.
For example methods cannot be used in XAML binding and everywhere else where a property is required.
Please note:
In C# (almost) everything is strongly typed hence once the class and the properties in it are defined you cannot set one of its property value to a different type of data and also you cannot change the type of the property. So by default you cannot avoid projection when you need some transformation. If you need all the properties - either the original value or the transformed value - you have to list all of them in the projection.
almost everything except dynamic
You can actually transform the type and the value of a property but only if it is defined as dynamic. For example this works below:
public class Order
{
public int Id { get; set; }
public string OrderNumber { get; set; }
// Original: decimal; Converted: string;
public dynamic Price { get; set; }
public string ShipperState { get; set; }
// Original: DateTime; Converted: string;
public dynamic Timestamp { get; set; }
}
public static class OrderExtensions
{
public static void Transform(this Order order)
{
if (order.Price.GetType() == typeof(decimal))
order.Price = order.Price.ToString("C", new CultureInfo("en-US"));
if (order.Timestamp.GetType() == typeof(DateTime))
order.Timestamp = order.Timestamp.ToString("MM/dd/yyyy H:mm");
}
}
class Program
{
static void Main(string[] args)
{
var originalList = new List<Order>()
{
new Order() { Id = 1, OrderNumber = "1", Price = 100m, Timestamp = DateTime.Now },
new Order() { Id = 2, OrderNumber = "2", Price = 200m, Timestamp = DateTime.Now },
new Order() { Id = 3, OrderNumber = "3", Price = 300m, Timestamp = DateTime.Now }
};
originalList.ForEach(order => order.Transform());
}
}
Although this example works there are some things to know:
dynamic type
This example looks like a hack, maybe it can be considered as a hack. :)
In this example the original Order objects are changed not their projection/clone/etc.
dynamic properties are not allowed in Entity Framework models as you cannot specify the SQL column type for them even using the methods of DbModelBuilder. I did not try it in other use-cases but it seems to be a very restricted possibility.
For dynamic properties there is no IntelliSense, so after typing order.Price. no list would appear with any method or property.
You have to use these properties very carefully as there is no compile-time check. Any typo or other mistake will throw an exception only during run-time.
If this option somehow fits the needs it might be worth implementing the conversion of the string value back to the original type.
That's all the update I could add to my original answer. Hope this is an acceptable answer to your comment.
I have the following model:
public class ViewDataItem
{
public string viewName { get; set; }
public UpdateIndicator updateIndicator { get; set; }
}
With the following enum:
public enum UpdateIndicator
{
Original,
Update,
Delete
}
And the following Validator:
public class ViewValidator : AbstractValidator<ViewDataItem>
{
public ViewValidator()
{
RuleFor(x => x.viewName).NotEmpty().WithMessage("View name must be specified");
RuleFor(x => x.updateIndicator).SetValidator(new UpdateIndicatorEnumValidator<UpdateIndicator>());
}
}
public class UpdateIndicatorEnumValidator<T> : PropertyValidator
{
public UpdateIndicatorEnumValidator() : base("Invalid update indicator") {}
protected override bool IsValid(PropertyValidatorContext context)
{
UpdateIndicator enumVal = (UpdateIndicator)Enum.Parse(typeof(UpdateIndicator), context.PropertyValue.ToString());
if (!Enum.IsDefined(typeof(UpdateIndicator), enumVal))
return false;
return true;
}
}
The code is in a WebAPI that receives data via JSON, deserialize it to an object and then validates, but for some reason I can send whatever I please in the updateIndicator, so long as I don't put in an integer value larger than the max index in the enum (i.e 1,2 or 3 works fine, but 7 will generate an error).
How can I get this to validate the input of the data I receive to see if that value is actually in the Enum?
Try the built-in IsInEnum()
RuleFor(x => x.updateIndicator).IsInEnum();
This checks if the provided enum value is within the range of your enum, if not, the validation will fail:
"'updateIndicator' has a range of values which does not include '7'."
The problem arises from the fact that the API model builder will convert what is sent to an enum. If a value isn't found, it doesn't populate it, and the default value is used (as it would be with any other property data type that isn't populated).
In order to easily tell if the value sent is a valid enum value, you should make your property nullable. That way, if a value isn't able to be parsed, it will be set to null. If you want to ensure that the property is set, just have your validator not allow null values for it.
public class ViewDataItem
{
public string viewName { get; set; }
public UpdateIndicator? updateIndicator { get; set; }
}
public class ViewValidator : AbstractValidator<ViewDataItem>
{
public ViewValidator()
{
RuleFor(x => x.viewName).NotEmpty().WithMessage("View name must be specified");
RuleFor(x => x.updateIndicator).NotNull();
}
}
Without setting the property to null, your model will always have a valid value when you have it. Alternatively, you could have the first value of your enum be a dummy value, but that would be a code smell. A null model property makes far more sense.
If you want to find out what the actual value that was sent to the API endpoint was, you'll need to look at creating an HTTP Handler, which is beyond the scope of this question.