Efficiently Sharing and Managing Extended Enums Across Multiple Projects
Table of content
· Problem
∘ Standalone/self-contained app
∘ How do we extend it to multiple projects?
· Solution: Initial problem with a standalone/self-contained app
∘ Ensuring identical behavior for enum and class
∘ Implementing custom JsonConverter
∘ Align each enum value with its specific functionality
· Solution: Multiple projects
∘ Genereic extended enum
∘ Configuration Pattern and Centralized Management
· Navigating Complexities: Extended enums in EF Core and OpenAPI
∘ EF Core Integration
∘ OpenAPI and Swagger integration
· GitHub sample project
Problem:
While trying to solve this problem, I scoured the internet for an end-to-end solution that was well-conceived and effectively addressed the issues at hand. Unfortunately, I couldn’t find one that met my criteria. This realization led me to write this article. Beyond just Extended Enums, you’ll discover insights into various topics such as Custom JsonConverters, TypeConverters, EF Core Value Converters, Swagger schema filters, and more.
Standalone/self-contained app:
Let’s begin with a standalone/self-contained app, exploring the challenges and potential solutions. Take, for instance, a project involving users with customizable fields. Users can create their own fields with predefined types and corresponding validations. Examine the code for a closer understanding:
User.cs
public class User
{
public User(Guid id, string name, List<FieldReference> fields)
{
Id = id;
Name = name;
Fields = fields;
}
public Guid Id { get; set; }
public string Name { get; set; }
public List<FieldReference> Fields { get; set; }
}
Field.cs
public class Field
{
public string Name { get; set; }
public FieldType Type { get; set; }
}
public enum FieldType
{
String,
Date,
Int,
Username,
PrimeNumber
}
public class FieldReference
{
public string Name { get; set; }
public object Value { get; set; }
public override string ToString()
{
return $"{Name}: {Value}";
}
}
- The
Field
class defines a basic structure for a custom field. - It has properties for
Name
(field name) andType
(field type), whereType
is an enumeration of possible field types such as String, Date, Int, Username, and PrimeNumber.
Also, let’s define a service that will map the enum value to its validation:
public class FieldService
{
public static bool IsFieldValueValid(Field field, object fieldValue)
{
return field.Type switch
{
FieldType.String => true,
FieldType.Date => DateTime.TryParse(fieldValue.ToString(), out _),
FieldType.Int => int.TryParse(fieldValue.ToString(), out _),
FieldType.Username => Regex.IsMatch(fieldValue.ToString(), "@\"^[a-zA-Z_@][a-zA-Z0-9_]*$\""),
FieldType.PrimeNumber => int.TryParse(fieldValue.ToString(), out var intValue) && IsPrime(intValue),
_ => throw new ArgumentOutOfRangeException()
};
}
}
This service offers fundamental validation checks for each field type, and while it’s functional, scalability poses a challenge. Consider the scenario where additional field types are introduced. In such cases, the developer must not only remember to add the new type but also ensure the corresponding validation check is included in the FieldService
class (the classic problem-solver move: just add a little #Note comment on it!). Moreover, if there's a decision to incorporate display options for the enum, this class could evolve into a comprehensive utility handling all aspects of enum logic, potentially transforming it into one big switch-case structure. So it's not the best/cleanest way to solve our case.
How do we extend it to multiple projects?
But the challenges don’t stop there. What if the plan is to establish a shared library and relocate the code there? The complexity arises when attempting to move the Field services. The goal is to empower each project to define its own field types along with all the associated utilities and checks, like this:
ProjectA: text, prime number, date
ProjectB: text, decimal, boolean
…
Solution: Initial problem with a standalone/self-contained app
Let’s start with solving the initial problem with a standalone/self-contained app. The objective is to establish a method for associating additional values with an enum, whether it be for validation checks, display purposes, or other functionalities. This is where the extended enumeration pattern, coupled with delegates, comes into play. In this case, we will leverage encapsulation as much as possible, reducing the chances of developers unintentionally creating less-than-ideal code.
The core concept involves transitioning the enum type to a class while trying to maintain identical behavior. The primary objective of an extended enum is to transform the enum
into a class
with properties. Following these adjustments, our Field
now appears as follows:
public class Field
{
public string Name { get; set; }
public FieldType Type { get; set; }
}
public class FieldType
{
public static readonly FieldType String = new(0, "String");
public static readonly FieldType Date = new(1, "Date");
public static readonly FieldType Int = new(2, "Int");
public static readonly FieldType Username = new(3, "Username");
public static readonly FieldType PrimeNumber = new(4, "PrimeNumber");
private FieldType(int value, string name)
{
Value = value;
Name = name;
}
public int Value { get; private set; }
public string Name { get; private set; }
}
In this snippet, FieldType
was changed from an enum to a class, containing two properties: Value
and Name
. Also, note that the constructor of FieldType
is private, preventing external or runtime instantiation(the same as it is for enums, you can’t create new values runtime). The static read-only fields represent the enum values (String, Date, etc.).
Ensuring identical behavior for enum and class:
As previously discussed, our aim is to ensure identical behavior for these two types (enum and class). Let’s consider those key scenarios:
- Comparing enums by value:
FieldType.Username != FieldType.PrimeNumber
- Enum.ToString must return name:
var name = FieldType.Username.ToString()
- Enum HashCode is calculated based on the value:
var dictionary = new Dictionary<FieldType, User>()
- Casting enum to int (and vice-versa):
int key = (int)FieldType.Username
- Serializing/Deserializing enum:
JsonSerializer.Serialize(FieldType.Username)
Those are the fundamental features of the enum that we strive to replicate for our class, ensuring a seamless replacement. Fortunately, we can implement all scenarios through the features provided by the .NET.
Let’s tackle these issues step by step.
1. Comparing enums by value
public override bool Equals(object? obj)
{
if (obj is not FieldType type) return false;
return Value == type.Value;
}
public static bool operator ==(FieldType f1, FieldType f2)
{
return f1.Equals(f2);
}
public static bool operator !=(FieldType f1, FieldType f2)
{
return !(f1 == f2);
}
The first method will be used when calling f1.Eqauls(f2)
. We’ve customized it to compare values instead of references(derived from object). The next two methods are invoked in scenarios like f1 == f2
. The overridden method is calling the first method, ensuring that the comparison is based on values.
2. Enum.ToString must return the name
public override string ToString()
{
return Name;
}
It’s a straightforward fix — just override the ToString
method to make sure it returns the name.
3. Enum HashCode is calculated based on the value
public override int GetHashCode()
{
return Value.GetHashCode();
}
In this scenario, we’re overriding the GetHashCode
function to operate based on the Value
. This adjustment ensures that calling f1.GetHashCode()
will return the hash code of its value.
4. Casting enum to int (and vice-versa)
public static implicit operator int(FieldType f)
{
return f.Value;
}
public static implicit operator FieldType(int value)
{
return _values.First(x => x.Value == value);
}
In this instance, we use implicit operator overriding to enable casting from int
to FieldType
, allowing expressions like (int)f1
or (FieldType)1
.
Implementing custom JsonConverter
5. Serializing/Deserializing enum
Without any code change, attempting to serialize FieldType
with JsonSerializer.Serialize(FieldType.Username)
produces the following JSON:
{"Value": 1,"Name": "Username"}
And yeah, this works right because the field type is a class (object).
To achieve this right behavior, we introduce a JsonConverter
and apply it to our class:
public class FieldTypeJsonConvertor : JsonConverter<FieldType>
{
public override FieldType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var values = FieldType.GetValues();
if (reader.TokenType == JsonTokenType.Number)
{
var intValue = reader.GetInt32();
return values.FirstOrDefault(x => x.Value == intValue);
}
if (reader.TokenType == JsonTokenType.String)
{
var stringValue = reader.GetString();
return values.FirstOrDefault(x => x.Name == stringValue);
}
throw new InvalidOperationException($"{reader.TokenType} cannot be converted to FieldType");
}
public override void Write(Utf8JsonWriter writer, FieldType value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value.Value);
}
}
Here
- The
Read
method handles deserialization, mapping values from the reader to ourFieldType
. It supports both integer (value) and string (name) cases. Otherwise, it's throwing an exception for unsupported types. - The
Write
method, on the other hand, is for serialization, writing the integer value in place of the field in JSON.
To apply this converter to the class, we use the [JsonConverter]
attribute:
[JsonConverter(typeof(FieldTypeJsonConvertor))]
public class FieldType : IEquatable<int>
{
...
}
These adjustments ensure that the serialization and deserialization of FieldType
produce the expected JSON representation.
Here are the results:
Applying these techniques successfully addresses the initial challenge (in the context of self-contained or stand-alone applications)
Align each enum value with its specific functionality:
As you remember we have a class FieldService
where we store a bunch of switch cases. To solve this problem we can add one more property to FieldType
class for validation, displaying, etc.
In our existing FieldService
, where a bunch of switch cases are stored, we can streamline the solution by introducing two new properties, ValidationFunction
and DisplayFunction
, to the FieldType
class. This addition adds possibilities such as validation and display inside the FieldType class.
[JsonConverter(typeof(FieldTypeJsonConvertor))]
public class FieldType : IEquatable<int>
{
private static readonly List<FieldType> _values = new();
public static readonly FieldType String = new(0, "String",
x => true,
x => x.ToString());
public static readonly FieldType Date = new(1, "Date",
x => DateTime.TryParse(x.ToString(), out _),
x => DateTime.Parse(x.ToString()).ToString("yyyy-M-d dddd"));
public static readonly FieldType Int = new(2, "Int",
x => int.TryParse(x.ToString(), out _));
public static readonly FieldType Username = new(3, "Username",
x => Regex.IsMatch(x.ToString(), @"^[a-zA-Z_@][a-zA-Z0-9_]*$"),
x => $"@{x}");
public static readonly FieldType PrimeNumber = new(4, "PrimeNumber",
x => Int.ValidationFunction(x) && IsPrime(int.Parse(x.ToString())));
private FieldType(int value, string name, Func<object, bool> validationFunction,
Func<object, string> displayFunction = null)
{
Value = value;
Name = name;
ValidationFunction = validationFunction;
DisplayFunction = displayFunction ?? (x => x.ToString());
_values.Add(this);
}
public int Value { get; }
public string Name { get; }
public Func<object, bool> ValidationFunction { get; }
public Func<object, string> DisplayFunction { get; }
...
}
These additions include two new properties, ValidationFunction
and DisplayFunction
, both of type Func
. The ValidationFunction
accepts an object as an argument and returns a boolean result, which is then provided during the initialization of each type. This ensures that the types are not only categorized by values but also associated with their specific validation and display functions. So all logic is stored in one place and developers can’t add new types without implementing validation and displaying functions.
Here is how it works in action:
This code snippet demonstrates the usage of the DisplayValue
method to showcase how different types of values can be validated and displayed based on their associated FieldType
. DisplayValue
method checks if the provided value
is valid for the given type
by invoking the ValidationFunction
associated with that FieldType
. If the value is valid, it prints the result of the DisplayFunction
associated with the type
. If the value is not valid, it prints a message indicating that the value was invalid for the specified type
.
And one more funny example of how all components work together:
Solution: Multiple projects
Imagine a scenario where we have two distinct projects: SharedLibrary and Program. The challenge involves relocating the FieldType
entity along with its associated functionality to the SharedLibrary
. However, the main problem that we need to solve here is to define the FieldType in SharedLibrary
but provide its enum values in Program1
or Program2
projects.
In the context of this challenge, Project1’s configuration encompasses enum values like text, prime number, and date, while Project2 configures enum values such as text, decimal, and boolean.
- Project1: text, prime number, date
- Project2: text, decimal, boolean
This approach proves exceptionally beneficial when creating a generic component with its core functionality in SharedLibrary
. It empowers individual projects, like Program1
and Program2
, to delineate custom FieldType
values, providing a flexible and tailored solution.
To delve deeper into the issue, let’s consider a scenario where the user’s core functionality is closely tied to FieldType
. The goal that we want to achieve is to centralize this functionality in SharedLibrary, allowing each project to define its unique set of FieldType
. So by the end the Project1
only needs to provide a few lines of configuration, ensuring that the user’s functionality easily integrates into the overall structure.
Genereic extended enum:
To make it easy to work with extended enums, we can create a base generic class:
The ExtendedEnumBase
class serves as the foundation for creating a generic extended enum. It encapsulates core functionalities that imitate enum behavior. It includes properties such as DisplayName
and Value
. Additionally, the class implements interfaces like IComparable
to enable comparison operations.
public class ExtendedEnumBase : IComparable
{
protected ExtendedEnumBase(string displayName, int value)
{
DisplayName = displayName;
Value = value;
}
public string DisplayName { get; protected set; }
public int Value { get; protected set; }
public int CompareTo(object obj)
{
return Value.CompareTo(obj as ExtendedEnumBase);
}
public override bool Equals(object? obj)
{
if (obj is not ExtendedEnumBase otherValue) return false;
return otherValue.Value == Value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override string ToString()
{
return DisplayName;
}
}
Then the FieldType
class:
public class FieldType : ExtendedEnumBase, IComparable
{
private static HashSet<FieldType>? _fieldTypes;
private FieldType(int value, string displayName, Func<object, bool> validationFunction) : base(displayName, value)
{
ValidationFunction = validationFunction;
}
public static IEnumerable<FieldType>? Values => _fieldTypes;
public Func<object, bool> ValidationFunction { get; }
public static bool operator ==(FieldType left, FieldType right)
{
return left?.Value == right?.Value;
}
public static bool operator !=(FieldType left, FieldType right)
{
return !(left == right);
}
public override bool Equals(object obj)
{
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static FieldType TryGet(int value)
{
CheckToBeInitialized();
return _fieldTypes.FirstOrDefault(x => x.Value == value);
}
public static FieldType TryGet(string displayName)
{
CheckToBeInitialized();
return _fieldTypes.FirstOrDefault(x =>
string.Equals(x.DisplayName, displayName, StringComparison.OrdinalIgnoreCase));
}
public static FieldType Get(int value)
{
CheckToBeInitialized();
return _fieldTypes.First(x => x.Value == value);
}
public static FieldType Get(string displayName)
{
CheckToBeInitialized();
return _fieldTypes.First(x => string.Equals(x.DisplayName, displayName, StringComparison.OrdinalIgnoreCase));
}
public static void Create(params (int value, string displayName, Func<object, bool> validation)[] values)
{
//If is not already initialized
if (CheckIfIsInitialized()) return;
values = values.OrderBy(x => x.value).ToArray();
_fieldTypes = new HashSet<FieldType>(values.Length);
foreach (var value in values)
{
var appRule = new FieldType(value.value, value.displayName, value.validation);
_fieldTypes.Add(appRule);
}
}
public static bool CheckIfIsInitialized()
{
return _fieldTypes != null;
}
private static void CheckToBeInitialized()
{
if (!CheckIfIsInitialized())
throw new InvalidOperationException("Field types must be initialized using FieldTypes.Create() method");
}
}
Breakdown:
- The
FieldType
class inherits fromExtendedEnumBase
, leveraging its core functionalities. - The class includes methods and some utility functions like
TryGet
andGet
to retrieveFieldType
instances based on value or display name. - The
Create
method is used for initializing the enum with specified values. It can be called only once, otherwise it will not change the values. The method also stores all the values inside_fieldTypes
HashSet, to be able - Various utility methods ensure that the initialization has occurred (
CheckIfIsInitialized
) and throw exceptions if necessary (CheckToBeInitialized
).
The distinction between a standalone and shared project lies in how FieldType
handles its values. In the standalone version, FieldType
stores all its values as static fields. However, in the shared project, these values are encapsulated within a HashSet
and the utility functions are provided to access these values.
Configuration Pattern and Centralized Management:
The next code introduces a configuration pattern through the ConfigureFieldTypes
class, allowing for a flexible and centralized setup of FieldType
values and associated validations.
public static class ConfigureFieldTypes
{
public static void Configure(Action<FieldTypeConfiguration> configurationAction)
{
var config = new FieldTypeConfiguration();
configurationAction(config);
FieldTypeConfiguration.ValidateValues();
}
}
public class FieldTypeConfiguration
{
internal FieldTypeConfiguration()
{ }
internal static void ValidateValues()
{
if (!FieldType.CheckIfIsInitialized())
throw new ArgumentException("Field types configuration is missing", nameof(FieldType));
}
public FieldTypeConfiguration ConfigureFieldTypes(
params (int Value, string DisplayName, Func<object, bool> validation)[] values)
{
FieldType.Create(values);
return this;
}
}
This class facilitates the configuration of FieldType
values and validations by accepting an Action
that operates on a FieldTypeConfiguration
instance(also note that the constructor is internal so users can’t override or create an instance of it without using the action method). The ValidateValues
method ensures that the FieldType
values are initialized after configuration.
So we can restrict the initialization of
FieldTypes
exclusively throughFieldTypeConfiguration
. This limitation is enforced by utilizing the providedaction
, which is exclusively accessible through theConfigureFieldTypes
class. Consequently, this process becomes centralized, ensuring that the configuration and initialization ofFieldTypes
are managed centrally through a standardized mechanism and only from one place.
And the usage will be:
internal class Program
{
private static void Main(string[] args)
{
ConfigureFieldTypes.Configure(config =>
{
config.ConfigureFieldTypes(
(0, "string", x => true),
(1, "boolean", x => bool.TryParse(x.ToString(), out _))
);
});
Console.WriteLine(FieldType.Get("string").Value); // 0
Console.WriteLine(FieldType.Get("boolean").ValidationFunction("false")); // true
}
}
This usage example demonstrates how the configuration pattern is applied. The Configure
method is invoked with a lambda expression specifying FieldType
configurations. This also allows for the dynamic registration of other services under the Configure
method (like services). Also, note that the shared library handles all the validations. This approach enables maintainability and promotes a centralized and modular configuration setup.
Navigating Complexities: Extended enums in EF Core and OpenAPI
Recall the challenges we previously addressed — ensuring consistent behavior for enums and classes. While we’ve made a working solution, additional complexities arise when considering the integration of EF Core and OpenAPI. Let’s dive deeper into these problems.
EF Core Integration:
Integrating our extended enum into an EF Core entity model introduces another challenge. Consider the following entity model:
Field.cs
public class Field
{
public int Id { get; set; }
public string Name { get; set; }
public FieldType Type { get; set; }
}
If FieldType
were a standard enum, EF Core would treat it as an integer and store numerical values in the database. Moreover, when executing queries, EF Core allows explicit enum values, like this:
_context.Fields.Where(x => x.FieldType == FieldType.String)
In this scenario, EF would generate an SQL query similar to the following:
SELECT * FROM "Fields" WHERE "FieldType" = 0
So, once again, we need to replicate this behavior for our extended enum. Achieving this can be done through EF core value converters. If you’re curious about how these converters work, I have a dedicated article on the topic that you can explore. Basically, value converters are functions designed to convert a specific type to and from the desired value, serving as a key component in resolving this particular challenge.
In our specific scenario, the initial type is FieldType
and the desired type is int
, the value converter will be like this:
public class FieldTypeValueConvertor : ValueConverter<FieldType, int>
{
private static readonly Expression<Func<FieldType, int>> ConvertFieldTypeToInt
= fieldType => fieldType.Value;
private static readonly Expression<Func<int, FieldType>> ConvertIntToFieldType
= intValue => FieldType.Get(intValue);
public FieldTypeValueConvertor(ConverterMappingHints? mappingHints = null)
: base(ConvertFieldTypeToInt, ConvertIntToFieldType, mappingHints)
{
}
}
Here, we define two static expressions, ConvertFieldTypeToInt
and ConvertIntToFieldType
, each dedicated to the appropriate value conversion. These expressions are then supplied to the base class constructor.
To implement the converter for our property, we apply it within the entity configuration during the model-building process. Here’s how you can achieve this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var entityBuilder = modelBuilder.Entity<Field>();
entityBuilder.Property(x => x.Type)
.HasConversion<FieldTypeValueConvertor>(); // <---
}
The results are as follows:
internal class Program
{
private static async Task Main(string[] args)
{
ConfigureFieldTypes.Configure(config =>
{
config.ConfigureFieldTypes(
(0, "string", x => true),
(1, "boolean", x => bool.TryParse(x.ToString(), out _))
);
});
var context = new AppDbContext();
await context.Database.MigrateAsync();
var field = new Field
{
Name = "IsEnabled",
Type = FieldType.Get("boolean")
};
await context.Fields.AddAsync(field);
await context.SaveChangesAsync();
}
}
After inserting a new field in the table we can see that Type is inserted as an integer (it’s Value
)
In the querying example, notice how we utilize the FieldType
enum directly without using numerical values. The code snippet looks like this:
var fields = await context.Fields
.Where(x => x.Type == FieldType.Get("boolean"))
.ToListAsync();
This approach leads to the generation of the following SQL script:
SELECT f."Id", f."Name", f."Type"
FROM "Fields" AS f
WHERE f."Type" = 1
The generated SQL script reflects the mentioned behavior, with the FieldType.Get("boolean")
translating into its corresponding numerical representation for the database query.
OpenAPI and Swagger integration:
Another problem will occur when trying to use an extended enum in Swagger and OpenAPI. Consider a WebAPI project with the following controller:
[ApiController]
[Route("[controller]")]
public class FieldsController : ControllerBase
{
private readonly AppDbContext _context;
public FieldsController()
{
_context = new AppDbContext();
}
[HttpGet]
public async Task<IEnumerable<Field>> Get()
{
return await _context.Fields.ToListAsync();
}
}
When we make the request we see those results:
The response appears to function correctly due to the applied JsonConverter
to the FieldType
class, which responsibly handles the mapping of values. However, an issue arises when examining the example value/schema in Swagger.
The schema shows the type
as an object with certain parameters. This inconsistency demands a solution to achieve identical behavior that must also be fixed.
Let's create another Configuration function in SharedLibrary ConfigureFieldTypes
class, accepting IServiceCollection
as an argument.
public static void ConfigureSharedLibrary(this IServiceCollection services, Action<FieldTypeConfiguration> configurationAction)
{
var config = new FieldTypeConfiguration();
configurationAction(config);
FieldTypeConfiguration.ValidateValues();
services.ConfigureSwaggerGen(c =>
{
//Add schema filter for class to show as enum values in swagger
c.SchemaFilter<FieldTypeSchemaFilter>();
});
}
The only addition here is the invocation of ConfigureSwaggerGen
method, introducing a new SchemaFilter
for defining a new Swagger schema. This SchemaFilter
enables us to modify the schema for a specific type. When not registered, Swagger uses the default schemas.
> The default SchemaFilter
marks FieldType
as an object, that’s why we see it as a JSON object in the schema.
The FieldTypeSchemaFilter
class serves to alter the default schema of FieldType
:
public class FieldTypeSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type != typeof(FieldType)) return;
schema.Type = "string";
schema.AdditionalPropertiesAllowed = true;
schema.Description = null;
schema.Properties = new Dictionary<string, OpenApiSchema>();
if (!context.SchemaRepository.Schemas.ContainsKey(nameof(FieldType)))
{
schema.Enum = FieldType.Values!.Select(v => new OpenApiString(v.Value.ToString()))
.ToList<IOpenApiAny>();
}
}
}
First, we filter out types that aren’t FieldType
, allowing the default SchemaFilter
to handle them. then we override the schema values, changing the type to “string” and adding schema.Enum
values from FieldType.Values
.
Here are the results:
The final hurdle in our journey is query parameters. Suppose we want to retrieve FieldType
from the query, as demonstrated in the following code snippet:
[HttpGet]
public async Task<IEnumerable<Field>> Get([FromQuery] FieldType? fieldType = null)
{
var query = _context.Fields.AsQueryable();
if (fieldType != null) query = query.Where(x => x.Type == fieldType);
return await query.ToListAsync();
}
When running errors will arise because Swagger struggles to construct a schema for this scenario. To tackle this, we introduce another converter: TypeConverter
. These converters enable the programmatic conversion of types.
public class FieldTypeTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(int) || sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return value switch
{
string stringValue => FieldType.TryGet(stringValue),
int intValue => FieldType.TryGet(intValue),
_ => base.ConvertFrom(context, culture, value)
};
}
}
Here, we declare that FieldType
can be converted from both int
and string
. The ConvertFrom
method is then defined for these two types. Applying the converter to our class is achieved using the attribute:
[TypeConverter(typeof(FieldTypeTypeConverter))]
public class FieldType : ExtendedEnumBase, IComparable
{ ... }
This approach ensures that Swagger can easily construct a schema for query parameters involving our extended enum — FieldType
.