Skip to main content

PMart.Enumeration by Martinho

Nuget / site data

NugetNuget GitHub last commit GitHub Repo stars

Details

Info

info

Name: PMart.Enumeration

Source code generator to easly create Enumeration classes.

Author: Martinho

NuGet: https://www.nuget.org/packages/PMart.Enumeration.Generator/

https://www.nuget.org/packages/PMart.Enumeration/

You can find more details at https://github.com/p-martinho/Enumeration

Source : https://github.com/p-martinho/Enumeration

Original Readme

note

NuGet NuGet Build status

PMart.Enumeration

This set of libraries provides base classes to implement Enumeration classes, based on string values. It enables the strongly typed advantages, while using string enumerations.

It has, also, the possibility to create new enumerations at runtime (let's call it Dynamic Enumerations).

What are Enumeration Classes?

Enumeration classes are alternatives to enum type in C#. They enable features of an object-oriented language without the limitations of the enum type.

They are useful, for instance, for business related enumerations on Domain-Driven Design (DDD).

For more information about enumeration classes, check the links on the section References.

NuGet Packages

PMart.Enumeration: The Enumeration base classes. NuGet

PMart.Enumeration.EFCore: The Entity Framework Core support for PMart.Enumeration. NuGet

PMart.Enumeration.JsonNet: The Newtonsoft Json.NET support for PMart.Enumeration. NuGet

PMart.Enumeration.SystemTextJson: The System.Text.Json support for PMart.Enumeration. NuGet

PMart.Enumeration.SwaggerGen: Support to generate Swagger documentation when using PMart.Enumeration. NuGet

PMart.Enumeration.Mappers: Mappers and mapping extensions for Enumerations (includes mapper for Mapperly). NuGet

PMart.Enumeration.Generator: A source generator to generate Enumeration classes from a few lines of code. NuGet

Installation

Install one or more of the available NuGet packages in your project.

Use your IDE or the command:

dotnet add package <package name>

Usage

An Enumeration is a class that holds a value of type string. Each Enumeration class should have declared one or more static instances to set the available enumeration members.

  • Create a new enumeration class by extending Enumeration<T>, where T is the class itself.
  • Add a private constructor, as in the bellow example.
  • Create a public static readonly instance of the class for each enumeration member.

Or you can use the Generator in PMart.Enumeration.Generator package to generate the code for you!

Here is a sample for communication types:

using PMart.Enumeration;

namespace Enumeration.Sample.Enumerations;

/// <summary>
/// The communication type enumeration.
/// </summary>
public class CommunicationType : Enumeration<CommunicationType>
{
public static readonly CommunicationType Email = new("Email");

public static readonly CommunicationType Sms = new("SMS");

public static readonly CommunicationType PushNotification = new("PushNotification");

private CommunicationType(string value) : base(value)
{
}
}

Now, you can use it as an enumeration class, type safe, with all its advantages and features:

public bool IsToSendEmail(CommunicationType communicationType)
{
return communicationType == CommunicationType.Email;
}

You can check some usage examples in the samples.

Features

The Enumeration classes enables the several features described bellow. For instance, you can add behavior, and/or you can use dynamic enumerations (created in runtime), etc.

Value

It is the string value that the enumeration class holds:

CommunicationType.Email.Value; // returns "Email"

The ToString() method also returns the value:

CommunicationType.Email.ToString(); // returns "Email"

GetMembers

Get all the enumerations from an enumeration class:

var allCommunicationTypes = CommunicationType.GetMembers(); // returns an IEnumerable<CommunicationType> with CommunicationType.Email, CommunicationType.Sms and CommunicationType.PushNotification
var communicationTypesCount = CommunicationType.GetMembers().Count(); // returns 3

The list of possible enumerations is a Lazy object behind the scene, and it is evaluated only if needed.

GetValues

Get all the possible values of an enumeration class:

var allCommunicationTypeValues = CommunicationType.GetValues(); // returns an IEnumerable<string> with "Email", "SMS" and "PushNotification"
var communicationTypeValuesCount = CommunicationType.GetValues().Count(); // returns 3

HasValue

Find out if there is any enumeration member with a specific value (ignoring letters case):

var hasValue = CommunicationType.HasValue("someUnknownValue"); // false
hasValue = CommunicationType.HasValue("Email"); // true
hasValue = CommunicationType.HasValue("EMAIL"); // true

GetFromValueOrDefault

Get an enumeration instance from a string that matches the value of the enumeration (ignoring letters case), or null when there isn't any enumeration with that value:

// Parse the string to Enumeration:
var communicationType = CommunicationType.GetFromValueOrDefault("email"); // returns CommunicationType.Email

// Verify if exists an enumeration with the value (GetFromValueOrDefault returns null if there isn't any enumeration with the value).
var isValid = communicationType is not null; // true

Note: When there's instances with equivalent values (same value ignoring case), the GetValueOrDefault can return any of the instances (is nondeterministic). Therefore, enumeration members with equivalent values are not recommended.

// Let's imagine we have these two members:
// public static readonly CommunicationType Email = new("Email");
// public static readonly CommunicationType EmailWithDifferentCase = new("EMAIL"); // same value, different case (this is not recommended)

var emailType = CommunicationType.GetFromValueOrDefault("Email"); // this may return CommunicationType.Email or CommunicationType.EmailWithDifferentCase (they have equivalent values)
var isSame = ReferenceEquals(emailType, CommunicationType.Email); // sometimes is true, sometimes is false, is nondeterministic
var isEqual = emailType == EmailWithDifferentCase; // always true. Even if they are different instances, they are equal. Check the Equality section bellow.

Equality

Two different instances of a type derived from Enumeration are equal if they are from the same enumeration type and if the value of both is equivalent, ignoring letters case.

// Let's imagine we have these two members:
// public static readonly CommunicationType Email = new("Email");
// public static readonly CommunicationType EmailWithDifferentCase = new("EMAIL"); // same value, different case (this is not recommended)

var isSame = ReferenceEquals(CommunicationType.Email, CommunicationType.EmailWithDifferentCase); // false (they are different instances)
var isEqual = CommunicationType.Email == CommunicationType.EmailWithDifferentCase; // true (they are different instances, but they have the same value, ignoring case)

It is also possible to test the equality between a string and an Enumeration. It also ignores the letters case. The string must be on the left side of the equality operator:

var isStringEqualToEnumeration = "email" == CommunicationType.Email; // true
isStringEqualToEnumeration = "EMAIL" == CommunicationType.Email; // true
var isStringNotEqualToEnumeration = "email" != CommunicationType.Email; // false
isStringNotEqualToEnumeration = "EMAIL" != CommunicationType.Email; // false

Switch

Since you have objects and not constant values (like in a enum), the switch statement can't be constructed the same way as for an enum, but you can, for example, use pattern matching this way:

private ISender? GetCommunicationSenderForCommunicationType(CommunicationType communicationType)
{
// A switch statement for pattern matching
return communicationType switch
{
_ when communicationType == CommunicationType.Email => _emailSender,
_ when communicationType == CommunicationType.PushNotification => _pushNotificationSender,
_ when communicationType == CommunicationType.Sms => _smsSender,
_ => null
};
}

Enumeration with Behavior

We can add custom methods to the Enumeration class (it's an object, after all).

Here is a simple example, with a method ParseMessage and with a property IsPhoneNumberRequired:

using PMart.Enumeration;

namespace Enumeration.Sample.Enumerations;

/// <summary>
/// The communication type enumeration.
/// </summary>
public class CommunicationTypeWithBehaviour : Enumeration<CommunicationTypeWithBehaviour>
{
public static readonly CommunicationTypeWithBehaviour Email = new("Email");

public static readonly CommunicationTypeWithBehaviour Sms = new("SMS");

public static readonly CommunicationTypeWithBehaviour PushNotification = new("PushNotification");

/// <summary>
/// Parses the message.
/// </summary>
/// <param name="message">The message content.</param>
/// <returns>The parsed message.</returns>
public string ParseMessage(string message)
{
return $"Message parsed by the communication type {this}: {message}";
}

/// <summary>
/// Gets a value indicating if this communication type requires phone number.
/// </summary>
/// <returns><c>true</c> if this communication type requires phone number; <c>false</c> otherwise.</returns>
public bool IsPhoneNumberRequired => this switch
{
_ when this == Sms => true,
_ when this == PushNotification => true,
_ => false
};

private CommunicationTypeWithBehaviour(string value) : base(value)
{
}
}

We can also use inheritance to add specific behavior or properties for each enumeration member in an Enumeration class. Check this example, where the communication type has subclasses with a specific implementation of ParseMessage() and IsPhoneNumberRequired:

using PMart.Enumeration;

namespace Enumeration.Sample.Enumerations;

/// <summary>
/// The communication type enumeration.
/// </summary>
public abstract class CommunicationTypeWithSpecificBehaviour : Enumeration<CommunicationTypeWithSpecificBehaviour>
{
public static readonly CommunicationTypeWithSpecificBehaviour Email = new EmailType();

public static readonly CommunicationTypeWithSpecificBehaviour Sms = new SmsType();

public static readonly CommunicationTypeWithSpecificBehaviour PushNotification = new PushNotificationType();

/// <summary>
/// Parses the message.
/// </summary>
/// <remarks>Each communication type, implements its own way of parsing the message.</remarks>
/// <param name="message">The message content.</param>
/// <returns>The parsed message.</returns>
public abstract string ParseMessage(string message);

/// <summary>
/// Gets a value indicating if this communication type requires phone number.
/// </summary>
/// <returns><c>true</c> if this communication type requires phone number; <c>false</c> otherwise.</returns>
public abstract bool IsPhoneNumberRequired { get; }

private CommunicationTypeWithSpecificBehaviour(string value) : base(value)
{
}

private sealed class EmailType : CommunicationTypeWithSpecificBehaviour
{
public EmailType() : base("Email")
{
}

/// <inheritdoc />
public override string ParseMessage(string message)
{
return $"<html>{message}</html>";
}

/// <inheritdoc />
public override bool IsPhoneNumberRequired => false;
}

private sealed class SmsType : CommunicationTypeWithSpecificBehaviour
{
public SmsType() : base("Sms")
{
}

/// <inheritdoc />
public override string ParseMessage(string message)
{
return $"Message encoded for SMS: {message}";
}

/// <inheritdoc />
public override bool IsPhoneNumberRequired => true;
}

private sealed class PushNotificationType : CommunicationTypeWithSpecificBehaviour
{
public PushNotificationType() : base("PushNotification")
{
}

/// <inheritdoc />
public override string ParseMessage(string message)
{
return $"Message encoded for push notification: {message}";
}

/// <inheritdoc />
public override bool IsPhoneNumberRequired => true;
}
}

Dynamic Enumerations

Instead of extending Enumeration class, you can extend the EnumerationDynamic class. The EnumerationDynamic class extends the Enumeration class, therefore, it has the same features. With this type, you will have an extra method that adds the possibility to create new EnumerationDynamic instances at runtime, if there isn't any enumeration member with a specific value.

To create an EnumerationDynamic is the same as Enumeration, but it requires a public empty constructor, in addition to the private constructor.

You can use the Generator in PMart.Enumeration.Generator package, that generates the code for you, and therefore you don't need to worry about the constructors.

Continuing with the communication types, here is an example using EnumerationDynamic:

using PMart.Enumeration;

namespace Enumeration.Sample.Enumerations;

/// <summary>
/// The communication type enumeration.
/// </summary>
public class CommunicationTypeDynamic : EnumerationDynamic<CommunicationTypeDynamic>
{
public static readonly CommunicationTypeDynamic Email = new("Email");

public static readonly CommunicationTypeDynamic Sms = new("SMS");

public static readonly CommunicationTypeDynamic PushNotification = new("PushNotification");

public CommunicationTypeDynamic()
{
}

private CommunicationTypeDynamic(string value) : base(value)
{
}
}

Now, you can use the method GetFromValueOrNew(string? value), that returns an instance of the enumeration type, or null if the provided value is null. If there is an enumeration with the provided value (ignoring letters case), it will return that instance, else it will create a new instance with the provided value and return it (or null if the provided value is null).

var a = CommunicationTypeDynamic.GetFromValueOrNew("Email"); // returns CommunicationTypeDynamic.Email
var b = CommunicationTypeDynamic.GetFromValueOrNew("EMAIL"); // returns CommunicationTypeDynamic.Email
var c = CommunicationTypeDynamic.GetFromValueOrNew("someUnknownType"); // returns new instance of CommunicationTypeDynamic, with value = "someUnknownType"
var d = CommunicationTypeDynamic.GetFromValueOrNew(null); // returns null

var aValue = a?.Value; // "Email"
var bValue = b?.Value; // "Email"
var cValue = c?.Value; // "someUnknownValue"
var dValue = d?.Value; // null

Note: Instances created with equivalent values are equal (check section Equality), but different instances:

var a = CommunicationTypeDynamic.GetFromValueOrNew("someUnknownType"); // returns a new instance of CommunicationTypeDynamic, with value = "someUnknownType"
var b = CommunicationTypeDynamic.GetFromValueOrNew("someUnknownType"); // returns another new instance of CommunicationTypeDynamic, with value = "someUnknownType"
var c = CommunicationTypeDynamic.GetFromValueOrNew("SOMEuNKNOWtTYPE"); // returns another new instance of CommunicationTypeDynamic, with value = "SOMEuNKNOWtTYPE"

var isAEqualToB = a == b; // true
var isAEqualToC = a == c; // true
var isBEqualToC = b == c; // true
var isASameInstanceThanB = ReferenceEquals(a, b); // false
var isASameInstanceThanC = ReferenceEquals(a, c); // false
var isBSameInstanceThanC = ReferenceEquals(b, c); // false

Note: when you create a new enumeration with EnumerationDynamic, that enumeration will not be added to the list of existent enumeration members:

var newCommunicationType = CommunicationTypeDynamic.GetFromValueOrNew("someUnknownType"); // returns a new instance of CommunicationTypeDynamic, with value = "someUnknownType"

var existsTheNewTypeOnCommunicationTypes = CommunicationTypeDynamic
.GetMembers()
.Any(ct => ct == newCommunicationType); // false

Why Dynamic Enumerations?

The EnumerationDynamic class can be useful when you want to accept values that are not in the declared enumerations or when you want to have the possibility to create new enumerations at runtime.

For example, an API A sends data to API B that then redirects the data to API C. All these APIs use enumeration classes, but API B don't care about the value, it just sends it to API C. So, using EnumerationDynamic on API B you don't need to deploy API B every time you had a new value to the enumeration on API A. Other way, using Enumeration instead of EnumerationDynamic, you would need to update API B in order to recognize the new enumeration member and send it to the API C.

You can check the example here.

EFCore Support

In EF Core, adding a property of type Enumeration or EnumerationDynamic to an entity requires setting the conversion to store the value of the enumeration on the database. The NuGet package PMart.Enumeration.EFCore has the required converters, you just need to add them to your model configuration. Check this sample:

For this entity:

public class CommunicationRecord
{
public Guid Id { get; set; }

public DateTime SentAt { get; set; }

public string To { get; set; } = null!;

public CommunicationType? Type { get; set; }

public CommunicationTypeDynamic? TypeDynamic { get; set; }
}

You need to configure it on model creating this way on your DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<CommunicationRecord>(e =>
{
e.Property(p => p.Type)
.HasConversion<EnumerationConverter<CommunicationType>>();

e.Property(p => p.TypeDynamic)
.HasConversion<EnumerationDynamicConverter<CommunicationTypeDynamic>>();
});
}

An usage sample:

public async Task<IEnumerable<CommunicationRecord>> GetCommunicationRecordsByType(CommunicationType communicationType)
{
var records = await _context.CommunicationRecords
.Where(r => r.Type == communicationType)
.ToListAsync();

return records;
}

Note: In a query, the case sensitivity is determined by the database provider. E.g., if you save the record using an EnumerationDynamic with value "Email", and then query the database using another instance of EnumerationDynamic with value "EMAIL", it is possible you get no results, depending on the database. For example, MS SQL Server is, by default, case-insensitive, so you would get the result.

Newtonsoft Json.NET Support

Using Newtonsoft Json.NET, if you need to serialize/deserialize objects that contain properties of type Enumeration, without any converters, the enumeration property would act like a regular object.

For example, using this model:

public class CommunicationRecord
{
public DateTime SentAt { get; set; }

public string To { get; set; } = null!;

public CommunicationType Type { get; set; } = null!;
}

The JSON without any custom JSON converters would be like:

{
"sentAt": "0001-01-01",
"to": "someone@email.com",
"communicationType": {
"value": "Email"
}
}

Probably, you would like a JSON where the CommunicationType works like an enum or a string value:

{
"sentAt": "0001-01-01",
"to": "someone@email.com",
"communicationType": "Email"
}

For that, you just need to use the custom converters available on the NuGet package PMart.Enumeration.JsonNet.

An example where the converter is added by attribute:

public class CommunicationRecord
{
public DateTime SentAt { get; set; }

public string To { get; set; } = null!;

[JsonConverter(typeof(EnumerationConverter<CommunicationType>))]
public CommunicationType Type { get; set; } = null!;
}

An example where the converter is added on the serializer converters:

public string SerializeCommunicationRecord(CommunicationRecord communicationRecord)
{
var json = JsonConvert.SerializeObject(communicationRecord, new EnumerationConverter<CommunicationType>());

return json;
}

For enumerations of type EnumerationDynamic, you can use the generic converter EnumerationDynamicConverter<T>.

When you have several enumeration types that you would like to register globally, instead of registering all the converters of type EnumerationConverter<T> (or EnumerationDynamicConverter<T>), one for each enumeration type, you can use the non-generic converter EnumerationConverter. This converter evaluates if the object is derived from Enumeration or EnumerationDynamic and handles it accordingly. It might be a little less performant.

public string SerializeCommunicationRecord(CommunicationRecord communicationRecord)
{
var json = JsonConvert.SerializeObject(communicationRecord, new EnumerationConverter());

return json;
}

System.Text.Json Support

Using System.Text.Json, if you need to serialize/deserialize objects that contain properties of type Enumeration, without any converters, the enumeration property would act like a regular object.

Again, for the same model example:

public class CommunicationRecord
{
public DateTime SentAt { get; set; }

public string To { get; set; } = null!;

public CommunicationType Type { get; set; } = null!;
}

The JSON without any custom JSON converters would be like:

{
"sentAt": "0001-01-01",
"to": "someone@email.com",
"communicationType": {
"value": "Email"
}
}

Probably, you would like a JSON where the CommunicationType works like a enum or a string value:

{
"sentAt": "0001-01-01",
"to": "someone@email.com",
"communicationType": "Email"
}

For that, you just need to use the JSON converter EnumerationConverterFactory available on the NuGet package PMart.Enumeration.SystemTextJson.

An example where the converter is added by attribute:

public class CommunicationRecord
{
public DateTime SentAt { get; set; }

public string To { get; set; } = null!;

[JsonConverter(typeof(EnumerationConverterFactory))]
public CommunicationType Type { get; set; } = null!;
}

An example where the converter is added on the serializer options:

public string SerializeCommunicationRecord(CommunicationRecord communicationRecord)
{
var serializerOptions = GetSerializerOptions();

var json = JsonSerializer.Serialize(communicationRecord, serializerOptions);

return json;
}

private JsonSerializerOptions GetSerializerOptions()
{
return new JsonSerializerOptions
{
Converters = { new EnumerationConverterFactory() }
};
}

Swagger Support

If you would like to add an enumeration property to a model from an API and would like to document it on Swagger like an enum, you should install the NuGet package PMart.Enumeration.SwaggerGen and add the schema filter EnumerationSchemaFilter to the Swagger options on your Program.cs (or Startup.cs), like in this example:

builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo {Version = "v1", Title = "Sample API"});

options.SchemaFilter<EnumerationSchemaFilter>();
});

Here's an example of the result:

Swagger sample 1

Swagger sample 2

Mapping

Map using built-in features

To map from a Enumeration or EnumerationDynamic to a string, it is very easy, as explained in the section Features:

var stringValue = CommunicationType.Email.Value; // "Email"
// Or:
var stringValue = CommunicationType.Email.ToString(); // "Email"

To map from a string to a Enumeration, is also straightforward, as explained in the section Features:

var enumeration = CommunicationType.GetFromValueOrDefault("Email"); // returns CommunicationType.Email

To benefit from the EnumerationDynamic features and map from a string to a EnumerationDynamic, as explained in the section Dynamic Enumerations, just use:

var enumeration = CommunicationTypeDynamic.GetFromValueOrNew("someUnknownType"); // returns a new CommunicationTypeDynamic with value "someUnknownType"

To map between different types of Enumeration or EnumerationDynamic, you can do it like this for Enumeration types:

var enumeration = OtherCommunicationType.GetFromValueOrDefault(communicationType.Value);

Or like this for EnumerationDynamic types:

var enumeration = OtherCommunicationTypeDynamic.GetFromValueOrNew(communicationType.Value);

Map using Extensions or Mappers

The NuGet package PMart.Enumeration.Mappers includes a set of extensions and mappers to help the mapping to/from string and between different types of Enumeration or EnumerationDynamic. And they are prepared for null values.

Here is an example using the extensions and the mappers to map between Enumeration and string:

public string? MapCommunicationTypeToStringUsingExtensions(CommunicationType communicationType)
{
return communicationType.MapToString();
}

public CommunicationType? MapStringToCommunicationTypeUsingExtensions(string communicationType)
{
return communicationType.MapToEnumeration<CommunicationType>();
}

public string MapCommunicationTypeToStringUsingMapper(CommunicationType communicationType)
{
return StringEnumerationMapper<CommunicationType>.MapToString(communicationType);
}

public CommunicationType MapStringToCommunicationTypeUsingMapper(string communicationType)
{
return StringEnumerationMapper<CommunicationType>.MapToEnumeration(communicationType);
}

Here is an example using the extensions and the mappers to map between EnumerationDynamic and string:

public string? MapCommunicationTypeToStringUsingExtensions(CommunicationTypeDynamic communicationType)
{
return communicationType.MapToString();
}

public CommunicationTypeDynamic? MapStringToCommunicationTypeUsingExtensions(string communicationType)
{
return communicationType.MapToEnumerationDynamic<CommunicationTypeDynamic>();
}

public string MapCommunicationTypeToStringUsingMapper(CommunicationTypeDynamic communicationType)
{
return StringEnumerationDynamicMapper<CommunicationTypeDynamic>.MapToString(communicationType);
}

public CommunicationTypeDynamic MapStringToCommunicationTypeUsingMapper(string communicationType)
{
return StringEnumerationDynamicMapper<CommunicationTypeDynamic>.MapToEnumerationDynamic(communicationType);
}

To map between different types of Enumeration, you can follow this example:

public OtherCommunicationType? MapToOtherTypeOfEnumeration(CommunicationType communicationType)
{
return OtherCommunicationType.GetFromValueOrDefault(communicationType.Value);
}

public OtherCommunicationType? MapToOtherTypeOfEnumerationUsingExtensions(CommunicationType communicationType)
{
// Usage: ...MapToEnumeration<the source type, the destination type>();
return communicationType.MapToEnumeration<CommunicationType, OtherCommunicationType>();
}

public OtherCommunicationType MapToOtherTypeOfEnumerationTypeUsingMapper(CommunicationType communicationType)
{
// Usage: EnumerationMapper<the source type, the destination type>.MapToEnumeration(...);
return EnumerationMapper<CommunicationType, OtherCommunicationType>.MapToEnumeration(communicationType);
}

And finally, to map between different types of Enumeration where the destination is an EnumerationDynamic, you can follow this example:

public OtherCommunicationTypeDynamic? MapToOtherTypeOfEnumeration(CommunicationType communicationType)
{
return OtherCommunicationTypeDynamic.GetFromValueOrNew(communicationType.Value);
}

public OtherCommunicationTypeDynamic? MapToOtherTypeOfEnumerationUsingExtensions(
CommunicationType communicationType)
{
// Usage: ...MapToEnumerationDynamic<the source type, the destination type>();
return communicationType.MapToEnumerationDynamic<CommunicationType, OtherCommunicationTypeDynamic>();
}

public OtherCommunicationTypeDynamic MapToOtherTypeOfEnumerationTypeUsingMapper(CommunicationType communicationType)
{
// Usage: EnumerationDynamicMapper<the source type, the destination type>.MapToEnumerationDynamic(...);
return EnumerationDynamicMapper<CommunicationType, OtherCommunicationTypeDynamic>.MapToEnumerationDynamic(
communicationType);
}

Using Mapperly

The Mapperly is a source generator for generating object mappings. To map objects that have properties of type Enumeration or EnumerationDynamic with Mapperly, you need to implement the mapping in the object mapper.

The NuGet package PMart.Enumeration.Mappers provides a set of mappers that can be used in Mapperly mappers, without the need to implement the mapping manually.

In this example, we have a source object that is mapped to a destination object, which requires mapping from Enumeration to string (from CommunicationType to string) and between different types of Enumeration (from CommunicationType to OtherCommunicationType):

public class SourceObject
{
public CommunicationType CommunicationType { get; set; } = null!;

public CommunicationType OtherCommunicationType { get; set; } = null!;
}

public class DestinationObject
{
public string CommunicationType { get; set; } = null!;

public OtherCommunicationType OtherCommunicationType { get; set; } = null!;
}

For this example, we need to create a Mapperly mapper, and we can use the mappers as external mappings, using the attribute [UseStaticMapper]:

// ...
using Riok.Mapperly.Abstractions;

namespace Enumeration.Mappers.Sample.Samples.Mapperly.Mappers;

[Mapper]
[UseStaticMapper(typeof(StringEnumerationMapper<CommunicationType>))]
[UseStaticMapper(typeof(EnumerationMapper<CommunicationType, OtherCommunicationType>))]
internal partial class SampleMapper
{
public partial DestinationObject SourceToDestination(SourceObject sourceModel);
}

For enumerations of type EnumerationDynamic, you can use the mappers StringEnumerationDynamicMapper and EnumerationDynamicMapper.

You can check the sample here.

Enumeration Generator

Creating a new Enumeration class is a little bit verbose. For instance, you can't forget to extend Enumeration<T> and to create the private constructor (else, it wouldn't compile anyway). Therefore, the package PMart.Enumeration.Generator was added to help on that. It is an incremental generator.

Generator Installation

Add the package to your project:

dotnet add package PMart.Enumeration.Generator

You need to keep the package PMart.Enumeration installed.

To any project referring that project don't get a reference to the PMart.Enumeration.Generator, you can add PrivateAssets="all" to the package reference. And you can also add ExcludeAssets="runtime", to avoid the PMart.Enumeration.Generator.dll file being copied to your build output (it is not required at runtime, it is a generator, so it works in compile time only):

<Project Sdk="Microsoft.NET.Sdk">

<!-- ... -->

<PackageReference Include="PMart.Enumeration" Version="3.1.0" />
<PackageReference Include="PMart.Enumeration.Generator" Version="3.1.0" PrivateAssets="all" ExcludeAssets="runtime" />

<!-- ... -->

</Project>

Generator Usage

To create a new Enumeration with the generator, it is easy:

  • Create a partial class, for the Enumeration class.
  • Add the EnumerationAttribute (namespace PMart.Enumeration.Generator.Attributes) on the class.
  • Add fields of type private static readonly string named with the prefix ValueFor (this prefix is one of the ways of doing it, as you can check next), that hold the values that will be used to create the enumeration members (check the bellow examples).

The non-fields or fields that are not private static readonly string are ignored.

For example, without the generator, the communication type enumeration was like this:

using PMart.Enumeration;

namespace Enumeration.Sample.Enumerations;

/// <summary>
/// The communication type enumeration.
/// </summary>
public class CommunicationType : Enumeration<CommunicationType>
{
public static readonly CommunicationType Email = new("Email");

public static readonly CommunicationType Sms = new("SMS");

public static readonly CommunicationType PushNotification = new("PushNotification");

private CommunicationType(string value) : base(value)
{
}
}

Using the generator and the prefix ValueFor (this prefix is one of the ways of doing it, as you can check next), it is just like this:

using PMart.Enumeration.Generator.Attributes;

namespace Enumeration.Generator.Sample.Enumerations;

/// <summary>
/// The communication type enumeration.
/// </summary>
[Enumeration]
public partial class CommunicationType
{
private static readonly string ValueForEmail = "Email";

private static readonly string ValueForSms = "SMS";

private static readonly string ValueForPushNotification = "PushNotification";
}

And the generated code will be something like this:

// <auto-generated />

namespace Enumeration.Generator.Sample.Enumerations
{
public partial class CommunicationType : Enumeration<CommunicationType>
{
public static readonly CommunicationType Email = new CommunicationType(ValueForEmail!);

public static readonly CommunicationType Sms = new CommunicationType(ValueForSms!);

public static readonly CommunicationType PushNotification = new CommunicationType(ValueForPushNotification!);

private CommunicationType(string value) : base(value)
{
}
}
}

If you don't worry about instantiating the enumeration members and your only concern is about the inheritance from Enumeration<T> and constructors, you can use the generator to build just that parts:

[Enumeration]
public partial class CommunicationType
{
public static readonly CommunicationType Email = new("Email");

public static readonly CommunicationType Sms = new("SMS");

public static readonly CommunicationType PushNotification = new("PushNotification");
}

You can check other examples in the samples.

The EnumerationMember Attribute

If you don't like the use of the prefix ValueFor to define the member names, you can use the EnumerationMemberAttribute to define the name of the enumeration member (but remember, it is not possible two fields have the same name, it will return a compilation error if you try to do that):

[Enumeration]
public partial class CommunicationType
{
[EnumerationMember("Email")]
private static readonly string EmailCode = "Email";

[EnumerationMember("Sms")]
private static readonly string SmsCode = "SMS";

[EnumerationMember("PushNotification")]
private static readonly string PushNotificationCode = "PushNotification";
}

The EnumerationIgnore Attribute

If, for some reason, you already have a field private static readonly string named ValueFor..., but you don't want it to be used to generate a new enumeration member, use the EnumerationIgnoreAttribute:

[Enumeration]
public partial class CommunicationType
{
private static readonly string ValueForEmail = "Email";

private static readonly string ValueForSms = "SMS";

private static readonly string ValueForPushNotification = "PushNotification";

[EnumerationIgnore]
private static readonly string ValueForSomeFieldThatShouldBeIgnored = "SomeValue";
}

Generate EnumerationDynamic

To generate an Enumeration class of type EnumerationDynamic<T>, enable the option IsDynamic of the EnumerationAttribute:

[Enumeration(IsDynamic = true)]
public partial class CommunicationTypeDynamic
{
private static readonly string ValueForEmail = "Email";

private static readonly string ValueForSms = "SMS";

private static readonly string ValueForPushNotification = "PushNotification";
}

The generated code will be something like this:

// <auto-generated />

namespace Enumeration.Generator.Sample.Enumerations
{
public partial class CommunicationTypeDynamic : EnumerationDynamic<CommunicationTypeDynamic>
{
public static readonly CommunicationTypeDynamic Email = new CommunicationTypeDynamic(ValueForEmail!);

public static readonly CommunicationTypeDynamic Sms = new CommunicationTypeDynamic(ValueForSms!);

public static readonly CommunicationTypeDynamic PushNotification = new CommunicationTypeDynamic(ValueForPushNotification!);

public CommunicationTypeDynamic()
{
}

private CommunicationTypeDynamic(string value) : base(value)
{
}
}
}

Generator Diagnostics

The generator tries to report errors when the user does common mistakes, namely about naming the enumeration members with names already in use. In some cases, there are no compilation errors on the user code. Without the diagnostics from the generator, the user would not know why the generator doesn't work.

For instance, assigning the same name for the enumeration member and for the field, the Enumeration class will not be generated and an error is reported:

[Enumeration]
public partial class CommunicationType
{
[EnumerationMember("Email")]
private static readonly string Email = "Email";
^^^^^ // Error ENUM0002: The name 'Email' of the Enumeration member is the same as the field name
}

Or, defining an invalid name for the enumeration member:

[Enumeration]
public partial class CommunicationType
{
// 123 is not a valid name for a class member in C#
[EnumerationMember("123")]
private static readonly string Email = "Email";
^^^^^ // Error ENUM0001: Invalid name for the Enumeration member in the EnumerationMemberAttribute
}

There are other diagnostics reported for different cases. All are of type Error with an ID like ENUMXXXX and with a descriptive message.

Generator Limitations

  • The .NET versions restrictions are:
    • .NET SDK: >= 8.0.100
    • MSBuild/Visual Studio: >= 17.8.
  • It does not work for abstract classes. In the example provided in Enumeration with behavior, we use an abstract class and subclasses. When using the generator, you can do the same without being abstract, check this sample.
  • It does not support nested classes (the usage of the EnumerationAttribute in a nested class does not have effect). But it supports nested namespaces.

Disclaimer

While the enumeration class is a good alternative to enum type, it is more complex and also .NET doesn't handle it as it handles enum (e.g. JSON des/serialization, model binding, etc.), requiring custom code. Please be aware that enumeration class may not fit your needs.

References

About

note

Constants as enumeration. With EFCore, Swagger and other implementations.

How to use

Example ( source csproj, source files )

This is the CSharp Project that references PMart.Enumeration

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

<PackageReference Include="PMart.Enumeration" Version="3.1.0" />
<PackageReference Include="PMart.Enumeration.Generator" Version="3.1.0" PrivateAssets="all" ExcludeAssets="runtime" />

</ItemGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
</Project>

Generated Files

Those are taken from $(BaseIntermediateOutputPath)\GX

// <auto-generated>
// This code was generated by the PMart.Enumeration.Generator source generator.
// </auto-generated>

#nullable enable

namespace DemoPMart
{
public partial class PersonType : global::PMart.Enumeration.Enumeration<global::DemoPMart.PersonType>
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("PMart.Enumeration.Generator", "3.1.0.0")]
public static readonly global::DemoPMart.PersonType Employee = new global::DemoPMart.PersonType(ValueForEmployee!);

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("PMart.Enumeration.Generator", "3.1.0.0")]
public static readonly global::DemoPMart.PersonType Manager = new global::DemoPMart.PersonType(ValueForManager!);

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("PMart.Enumeration.Generator", "3.1.0.0")]
private PersonType(string value) : base(value)
{
}
}
}

Usefull

Download Example (.NET C# )

Share PMart.Enumeration

https://ignatandrei.github.io/RSCG_Examples/v2/docs/PMart.Enumeration

In the same category (Enum) - 5 other generators

CredFetoEnum

EnumClass

EnumUtilities

FusionReactor

NetEscapades.EnumGenerators