PMart.Enumeration by Martinho
Nuget / site data
Details
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
Original Readme
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.
PMart.Enumeration.EFCore: The Entity Framework Core support for PMart.Enumeration
.
PMart.Enumeration.JsonNet: The Newtonsoft Json.NET support for PMart.Enumeration
.
PMart.Enumeration.SystemTextJson: The System.Text.Json support for PMart.Enumeration
.
PMart.Enumeration.SwaggerGen: Support to generate Swagger documentation when using PMart.Enumeration
.
PMart.Enumeration.Mappers: Mappers and mapping extensions for Enumerations (includes mapper for Mapperly).
PMart.Enumeration.Generator: A source generator to generate Enumeration classes from a few lines of code.
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>
, whereT
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 ofEnumerationDynamic
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:
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
(namespacePMart.Enumeration.Generator.Attributes
) on the class. - Add fields of type
private static readonly string
named with the prefixValueFor
(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 anabstract
class and subclasses. When using the generator, you can do the same without beingabstract
, 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
- Enumeration Classes:
- Incremental Generators:
About
Constants as enumeration. With EFCore, Swagger and other implementations.
How to use
Example ( source csproj, source files )
- CSharp Project
- Program.cs
- Person.cs
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>
This is the use of PMart.Enumeration in Program.cs
using DemoPMart;
var personType= PersonType.GetFromValueOrDefault("test");
Console.WriteLine(personType?.Value??"null");
personType = PersonType.GetFromValueOrDefault("manager");
Console.WriteLine(personType!.Value);
Console.WriteLine(PersonType.Manager == personType);
This is the use of PMart.Enumeration in Person.cs
using PMart.Enumeration.Generator.Attributes;
namespace DemoPMart;
[Enumeration]
public partial class PersonType
{
private static readonly string ValueForEmployee = "Employee";
private static readonly string ValueForManager = "Manager";
}
Generated Files
Those are taken from $(BaseIntermediateOutputPath)\GX
- PersonType.g.cs
// <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