Efficiently Sharing and Managing Extended Enums Across Multiple Projects

Gor Grigoryan
17 min readDec 24, 2023

--

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) and Type (field type), where Type 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 our FieldType. 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:

Serializing
Deserializing

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:

  1. The FieldType class inherits from ExtendedEnumBase, leveraging its core functionalities.
  2. The class includes methods and some utility functions like TryGet and Get to retrieve FieldType instances based on value or display name.
  3. 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
  4. 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 through FieldTypeConfiguration. This limitation is enforced by utilizing the provided action, which is exclusively accessible through the ConfigureFieldTypes class. Consequently, this process becomes centralized, ensuring that the configuration and initialization of FieldTypes 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.

How does the value converter work?

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.

GitHub sample project:

The whole project can be found on GitHub.

--

--

Gor Grigoryan
Gor Grigoryan

No responses yet