SoFunction
Updated on 2025-03-02

Briefly describe the advanced tactics of C# enumeration

At the beginning of the article, I will give you an interview question:

When designing a database for a small project (assuming MySQL), if you add a field (Roles) to the user table to store the user's role, what type would you set for this field? Tip: Consider that roles need to be represented by enumerations during backend development, and a user may have multiple roles.

The first answer that comes into your mind may be: varchar type, which uses separator to store multiple roles, such as 1|2|3 or 1,2,3 to indicate that the user has multiple roles. Of course, if the number of roles may exceed single digits, considering the convenience of querying the database (such as using INSTR or POSITION to determine whether the user contains a role), the value of the role must start at least with the number 10. The plan is feasible, but it is not too simple. Is there a better plan? The better answer is integers (int, bigint, etc.), the advantage is that it is more convenient to write SQL query conditions, and it is better than varchar in terms of performance and space. But an integer is just a number after all, how do you represent multiple characters? You who think of binary bit operations should have an answer in your mind. And keep the answer in your mind, and then read this article, you may have unexpected gains, because you may encounter a series of problems in actual applications. In order to better explain the following questions, let’s first review the basic knowledge of enumeration.

Enumeration basics

The purpose of an enum type is to limit its variables to be valued from a limited number, which corresponds to a number that starts at 0 by default and increments from this. For example:

public enum Days
{
  Sunday, Monday, Tuesday, // ...
}

Where Sunday's value is 0, Monday's value is 1, and so on. In order to see the value represented by each member at a glance, it is generally recommended to write out the member values ​​displayed, and do not omit it:

public enum Days
{
  Sunday = 0, Monday = 1, Tuesday = 2, // ...
}

The type of C# enumeration member is the int type by default. Through inheritance, the enumeration member can be declared as other types, such as:

public enum Days : byte
{
  Monday = 1,
  Tuesday = 2,
  Wednesday = 3,
  Thursday = 4,
  Friday = 5,
  Saturday = 6,
  Sunday = 7
}

The enum type must be inherited from byte, sbyte, short, ushort, int, uint, long and ulong, and cannot be other types. Here are some common usages of enums (the above Days enumeration is an example):

// Enumeration to stringstring foo = (); // "Saturday"
string foo = (typeof(Days), 6); // "Saturday"
// String to enum("Tuesday", out Days bar); // true, bar = 
(Days)(typeof(Days), "Tuesday"); // 

// Enumeration to numbersbyte foo = (byte); // 1
// Number to enumerationDays foo = (Days)2; // 

// Get the number type to which the enum belongsType foo = (typeof(Days))); // 

// Get all enumeration membersArray foo = (typeof(MyEnum);
// Get the field names of all enumeration membersstring[] foo = (typeof(Days));

Also, it is worth noting that enumerations may get unexpected values ​​(values ​​have no corresponding members). for example:

Days d = (Days)21; // No errors will be reported(typeof(Days), d); // false

Even if the enum has no member with a value of 0, its default value is always 0.

var z = default(Days); // 0

Enumerations can add useful auxiliary information to members through Description, Display and other features, such as:

public enum ApiStatus
{
  [Description("success")]
  OK = 0,
  [Description("Resource not found")]
  NotFound = 2,
  [Description("access denied")]
  AccessDenied = 3
}

static class EnumExtensions
{
  public static string GetDescription(this Enum val)
  {
    var field = ().GetField(());
    var customAttribute = (field, typeof(DescriptionAttribute));
    if (customAttribute == null) { return (); }
    else { return ((DescriptionAttribute)customAttribute).Description; }
  }
}

static void Main(string[] args)
{
  (()); // "success"}

I think the above already contains most of the enumeration knowledge we use in daily life. Let’s continue to return to the user role storage problem mentioned at the beginning of the article.

User role storage issues

Let's first define an enum type to represent two user roles:

public enum Roles
{
  Admin = 1,
  Member = 2
}

In this way, if a user has both Admin and Member roles, then the Roles field of the User table should be stored 3. Then the question is, how should I write SQL if I query all users with the Admin role? For basic programmers, this problem is very simple, just use bit operator logic and ('&') to query.

SELECT * FROM `User` WHERE `Roles` & 1 = 1;

Similarly, to query users who have both roles, SQL statements should be written like this:

SELECT * FROM `User` WHERE `Roles` & 3 = 3;

This is how to use C# to implement query for this SQL statement (for simplicity, Dapper is used here):

public class User
{
  public int Id { get; set; }
  public Roles Roles { get; set; }
}

<User>(
  "SELECT * FROM `User` WHERE `Roles` & @roles = @roles;",
  new { roles =  |  });

Correspondingly, in C#, you can judge whether the user has a certain role:

// Method 1if (( &amp; ) == )
{
  // Do what an administrator can do}

// Method 2if (())
{
  // Do what an administrator can do}

Similarly, in C# you can perform arbitrary logical operations on enums, such as removing roles from an enum variable:

var foo =  | ;
var bar = foo & ~;

This solves the problem of using integers to store multiple roles mentioned in the article. Whether it is a database or C# language, it is feasible in operation and is also very convenient and flexible.

Enumerated Flags Features

Below we provide a method to query the user through roles and demonstrate how to call it, as follows:

public IEnumerable&lt;User&gt; GetUsersInRoles(Roles roles)
{
  _logger.LogDebug(());
  _connection.Query&lt;User&gt;(
    "SELECT * FROM `User` WHERE `Roles` &amp; @roles = @roles;",
    new { roles });
}

// Call_repository.GetUsersInRoles( | );

The value of | is 3. Since there is no field with a value of 3 in the Roles enum type, the roles parameter in the method shows 3. 3 This information is not friendly to us to debug or print logs. Within the method, we do not know what this 3 represents. To solve this problem, C# enumeration has a very useful feature: FlagsAtrtribute.

[Flags]
public enum Roles
{
  Admin = 1,
  Member = 2
}

After adding this Flags feature, when we debug the GetUsersInRoles(Roles roles) method, the value of the roles parameter will be displayed as Admin|Member. Simply put, the difference between adding or not adding Flags is:

var roles =  | ;
(()); // "3", no Flags feature(()); // "Admin, Member", has the Flags feature

I think adding Flags features to enums should be regarded as a best practice in C# programming. Try to add Flags features when defining enums.

Resolve enum value conflict: Power of 2

At this point, everything seems to be fine with the enum type Roles, but what would happen if we wanted to add a role: Mananger now? According to the rule of incrementing numeric values, the Manager's value should be set to 3.

[Flags]
public enum Roles
{
  Admin = 1,
  Member = 2,
  Manager = 3
}

Can the Manager value be set to 3? Obviously not, because the value of Admin and Member perform bit or logical operations (i.e., Admin | Member) is also 3, indicating that they have both roles, which conflicts with Manager. So how to set values ​​to avoid conflicts? Since the binary logical operation "OR" will conflict with member values, it will be solved using the rules of logical operation or. We know that the logic of the "or" operation is that as long as a 1 appears on both sides, the result will be 1. For example, the result of 1|1 and 1|0 are both 1, and only the result of 0|0 is 0. Then we need to avoid any two values ​​appearing 1 at the same position. According to the characteristics of binary system full 2 ​​to 1, just ensure that the enumeration's values ​​are powers of 2. for example:

1:  00000001
2:  00000010
4:  00000100
8:  00001000
If it increases in the future, it will be 16, 32, 64..., and no matter how the values ​​are added, they will not conflict with any value of the member. This problem is solved, so we need to define the value of the Roles enum as follows:

[Flags]
public enum Roles
{
  Admin = 1,
  Member = 2,
  Manager = 4,
  Operator = 8
}

However, when defining values, you should make some calculations in your mind. If you want to be lazy, you can use the following "displacement" method to define it:

[Flags]
public enum Roles
{
  Admin  = 1 << 0,
  Member  = 1 << 1,
  Manager = 1 << 2,
  Operator = 1 << 3
}

Just increment the value downwards. If you have a good reading experience, it is not easy to make mistakes. The two methods are equivalent. The calculation of constant displacement is performed at compile time, so there is no additional overhead.

Summarize

This article triggers a series of thoughts on enumeration through a small interview question. In small systems, it is very common to store user roles directly in user tables. Setting the role field as an integer (such as int) is a better design solution. But at the same time, some best practices should be taken into account, such as using the Flags feature to help better debug and log output. Various potential problems in actual development should also be considered, such as the problem of multiple enum values ​​performing or ('|') operations conflicting with member values.

This is the end of this article about briefly describing the advanced tactics of C# enumeration. For more related C# enumeration content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!