Skip to content

Commit

Permalink
Smart flag enum (#160)
Browse files Browse the repository at this point in the history
* Added SmartFlagEnum Functionality

* Added AutoFixture Support for SmarFlagEnum Type

* SmartFlagEnum 1.0. Serialization and AutoFixture Utilities and UnitTests Complete.

* Return all flags for underlying type MaxValue

When FromValue() is called on a SmartFlagEnum with the corresponding MaxValue of the base tyoe as the value agrument, a list of all defined enum values is returned. This is similar to passing negative one.

* Pulled In Upstream Changes and Resolved Conflicts, Files Moved To New Folder Structure

* Missing nuget packages for integration projects (#62)

* current progress

* Move test project to solution and create

Directory.Build.props files to simplify the configuration process

* Set up CI with Azure Pipelines

[skip ci]

* Remove redundant PackageId from

Directory.Build.props

* Fix LangVersion property

* Fix unit test project missing source project

references

* Update test project path

* Update azure-pipelines.yml for Azure Pipelines

* Remove duplicate tasks

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Remove Benchmarks from pack process by ading

<GeneratePackageOnBuild>false</GeneratePackageOnBuild>

* Make protram and main method public to avoid

error CS5001 on azure pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Reorder project structure

-Bring solution to the root path
-Create benchmarks folder and move project
-Move test projects from src/test to test
-Reorder projects inside solutions
-Craete Directory.Build.props for tests projects and propagate code
coverage and commons nuget packages.
-Update azure .ylm pipeline to produce code coverage diagrams and result

* Fix solution items

Co-authored-by: Steve Smith <steve@kentsmiths.com>
Co-authored-by: Daniel Meza <dmeza@speedsol.com>

* Bump MessagePack from 1.7.3.4 to 1.9.11 in /src/SmartEnum.MessagePack (#68)

Bumps [MessagePack](https://github.com/neuecc/MessagePack-CSharp) from 1.7.3.4 to 1.9.11.
- [Release notes](https://github.com/neuecc/MessagePack-CSharp/releases)
- [Commits](MessagePack-CSharp/MessagePack-CSharp@v1.7.3.4...v1.9.11)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Pulled In Upstream Changes and Resolved Conflicts, Files Moved To New Folder Structure

* Merge branch 'master' into smart-flag-enum

* adding System.Text.Json converters for SmartFlagEnum

Co-authored-by: Steve Smith <steve@kentsmiths.com>
Co-authored-by: AFMHorizon <52022448+AFM-Horizon@users.noreply.github.com>
Co-authored-by: Suhaib <suhaibhasan@gmail.com>
Co-authored-by: Daniel Meza <daniel.meza2011@gmail.com>
Co-authored-by: Daniel Meza <dmeza@speedsol.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
7 people authored Feb 2, 2022
1 parent f1ee0a6 commit 674d281
Show file tree
Hide file tree
Showing 67 changed files with 4,530 additions and 32 deletions.
262 changes: 262 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,267 @@ testEnumVar

N.B. For performance critical code the fluent interface carries some overhead that you may wish to avoid. See the available [benchmarks](src/SmartEnum.Benchmarks) code for your use case.

### SmartFlagEnum

Support has been added for a `Flag` functionality.
This feature is similar to the behaviour seen when applying the `[Flag]` attribute to Enums in the .NET Framework
All methods available on the `SmartFlagEnum` class return an `IEnumerable<SmartFlagEnum>` with one or more values depending on the value provided/method called.
Some Functionality is shared with the original SmartEnum class, listed below are the variations.

### Setting SmartFlagEnum Values

When setting the values for a `SmartFlagEnum` It is imperative to provide values as powers of two. If at least one value is not set as power of two or two or more power of two values are provided inconsecutively (eg: 1, 2, no four!, 8) a `SmartFlagEnumDoesNotContainPowerOfTwoValuesException` will be thrown.

```csharp
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum Bpay = new SmartFlagTestEnum(nameof(Bpay), 4);
public static readonly SmartFlagTestEnum Paypal = new SmartFlagTestEnum(nameof(Paypal), 8);
public static readonly SmartFlagTestEnum BankTransfer = new SmartFlagTestEnum(nameof(BankTransfer), 16);

public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
```

This behaviour can be disabled by applying the `AllowUnsafeFlagEnumValuesAttribute` to the smart enum class. Note: If power of two values are not provided the SmarFlagEnum will not behave as expected!

```csharp
[AllowUnsafeFlagEnumValues]
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum Bpay = new SmartFlagTestEnum(nameof(Bpay), 4);
public static readonly SmartFlagTestEnum Paypal = new SmartFlagTestEnum(nameof(Paypal), 8);
public static readonly SmartFlagTestEnum BankTransfer = new SmartFlagTestEnum(nameof(BankTransfer), 16);

public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
```

`Combination` values can be provided explicitly and will be returned in place of the multiple flag values that would have been returned from the `FromValue()` method.

```csharp
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum CardAndCash = new SmartFlagTestEnum(nameof(CardAndCash), 3); -- Explicit `Combination` value
public static readonly SmartFlagTestEnum Bpay = new SmartFlagTestEnum(nameof(Bpay), 4);
public static readonly SmartFlagTestEnum Paypal = new SmartFlagTestEnum(nameof(Paypal), 8);
public static readonly SmartFlagTestEnum BankTransfer = new SmartFlagTestEnum(nameof(BankTransfer), 16);

public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
```

These explicit values can be provided above the highest allowable flag value without consequence, however attempting to access a value that is higher than the maximum flag value that has not explicitly been provided (for example 4) will cause a `SmartEnumNotFoundException` to be thrown.

```csharp
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum AfterPay = new SmartFlagTestEnum(nameof(AfterPay), 5);

public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}

var myFlagEnums = FromValue(3) -- Works!
-and-
var myFlagEnums = FromValue(5) -- Works!
-but-
Var myFlagEnums = FromValue(4) -- will throw an exception :(
```

A Negative One (-1) value may be provided as an `All` value. When a value of -1 is passed into any of the `FromValue()` methods an IEnumerable containing all values (excluding 0) will be returned.
If an explicit `Combination` value exists with a value of -1 this will be returned instead.

```csharp
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum All = new SmartFlagTestEnum(nameof(All), -1);
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum Bpay = new SmartFlagTestEnum(nameof(Bpay), 4);
public static readonly SmartFlagTestEnum Paypal = new SmartFlagTestEnum(nameof(Paypal), 8);
public static readonly SmartFlagTestEnum BankTransfer = new SmartFlagTestEnum(nameof(BankTransfer), 16);

public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
```

### Usage - (SmartFlagEnum)

```csharp
public abstract class EmployeeType : SmartFlagEnum<EmployeeType>
{
public static readonly EmployeeType Director = new DirectorType();
public static readonly EmployeeType Manager = new ManagerType();
public static readonly EmployeeType Assistant = new AssistantType();

private EmployeeType(string name, int value) : base(name, value)
{
}

public abstract decimal BonusSize { get; }

private sealed class DirectorType : EmployeeType
{
public DirectorType() : base("Director", 1) { }

public override decimal BonusSize => 100_000m;
}

private sealed class ManagerType : EmployeeType
{
public ManagerType() : base("Manager", 2) { }

public override decimal BonusSize => 10_000m;
}

private sealed class AssistantType : EmployeeType
{
public AssistantType() : base("Assistant", 4) { }

public override decimal BonusSize => 1_000m;
}
}

public class SmartFlagEnumUsageExample
{
public void UseSmartFlagEnumOne()
{
var result = EmployeeType.FromValue(3).ToList();

var outputString = "";
foreach (var employeeType in result)
{
outputString += $"{employeeType.Name} earns ${employeeType.BonusSize} bonus this year.\n";
}

=> "Director earns $100000 bonus this year.\n"
"Manager earns $10000 bonus this year.\n"
}

public void UseSmartFlagEnumTwo()
{
EmployeeType.FromValueToString(-1)
=> "Director, Manager, Assistant"
}

public void UseSmartFlagEnumTwo()
{
EmployeeType.FromValueToString(EmployeeType.Assistant | EmployeeType.Director)
=> "Director, Assistant"
}
}

```

### FromName()

Access an `IEnumerable` of enum instances by matching a string containing one or more enum names seperated by commas to its `Names` property:

```csharp
var myFlagEnums = TestFlagEnum.FromName("One, Two");
```

Exception `SmartEnumNotFoundException` is thrown when no names are found. Alternatively, you can use `TryFromName` that returns `false` when no names are found:

```csharp
if (TestFlagEnum.TryFromName("One, Two", out var myFlagEnums))
{
// use myFlagEnums here
}
```

Both methods have a `ignoreCase` parameter (the default is case sensitive).

### FromValue()

Access an `IEnumerable` of enum instances by matching a value:

```csharp
var myFlagEnums = TestFlagEnum.FromValue(3);
```

Exception `SmartEnumNotFoundException` is thrown when no values are found. Alternatively, you can use `TryFromValue` that returns `false` when values are not found:

```csharp
if (TestFlagEnum.TryFromValue(3, out var myFlagEnums))
{
// use myFlagEnums here
}
```

Note: Negative values other than (-1) passed into this method will cause a `NegativeValueArgumentException` to be thrown, this behaviour can be disabled by applying the `AllowNegativeInput` attribute to the desired `SmartFlagEnum` class.

```csharp
[AllowNegativeInput]
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);

public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
```

Note: `FromValue()` will accept any input that can be succesfully parsed as an integer. If an invalid value is supplied it will throw an `InvalidFlagEnumValueParseException`.

### FromValueToString()

Return a string representation of a series of enum instances name's:

```csharp
var myFlagEnumString = TestFlagEnum.FromValueToString(3);
```

Exception `SmartEnumNotFoundException` is thrown when no values are found. Alternatively, you can use `TryFromValueToString` that returns `false` when values are not found:

```csharp
if (TestFlagEnum.TryFromValueToString(3, out var myFlagEnumsAsString))
{
// use myFlagEnumsAsString here
}
```

Note: Negative values other than (-1) passed into this method will cause a `NegativeValueArgumentException` to be thrown, this behaviour can be disabled by applying the `AllowNegativeInput` attribute to the desired `SmartFlagEnum` class.

### BitWiseOrOperator

The `FromValue()` methods allow the Or ( | ) operator to be used to `add` enum values together and provide multiple values at once.

```csharp
var myFlagEnums = TestFlagEnum.FromValue(TestFlagEnum.One | TestFlagEnum.Two);
```

This will only work where the type of the `SmartFlagEnum` has been specified as `Int32` or else can be explicitly cast as an `Int32`.

```csharp
var myFlagEnums = TestFlagEnumDecimal.FromValue((int)TestFlagEnum.One | (int)TestFlagEnum.Two);
```

### Persisting with EF Core 2.1 or higher

EF Core 2.1 introduced [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) which can be used to map SmartEnum types to simple database types. For example, given an entity named `Policy` with a property `PolicyStatus` that is a SmartEnum, you could use the following code to persist just the value to the database:
Expand Down Expand Up @@ -431,6 +692,7 @@ uses the `Value`:
}
```

Note: The SmartFlagEnum works identically to the SmartEnum when being Serialized and Deserialized.
## References

- [Listing Strongly Typed Enums...)](https://ardalis.com/listing-strongly-typed-enum-options-in-c)
Expand Down
7 changes: 7 additions & 0 deletions SmartEnum.sln
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FA199ECB-5F2
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{5952C160-ABAA-4302-9150-0928E82545B1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartFlagEnum.UnitTests", "test\SmartFlagEnum.UnitTests\SmartFlagEnum.UnitTests.csproj", "{A927CEF1-A29C-4A8B-A590-7907ED93231F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartEnum.SystemTextJson", "src\SmartEnum.SystemTextJson\SmartEnum.SystemTextJson.csproj", "{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartEnum.SystemTextJson.UnitTests", "test\SmartEnum.SystemTextJson.UnitTests\SmartEnum.SystemTextJson.UnitTests.csproj", "{3EBD6CA5-CF2C-4350-922E-CEE2AE01445A}"
Expand Down Expand Up @@ -120,6 +122,10 @@ Global
{66EF3D1A-32AE-4B28-BC3E-7441615722A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66EF3D1A-32AE-4B28-BC3E-7441615722A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66EF3D1A-32AE-4B28-BC3E-7441615722A9}.Release|Any CPU.Build.0 = Release|Any CPU
{A927CEF1-A29C-4A8B-A590-7907ED93231F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A927CEF1-A29C-4A8B-A590-7907ED93231F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A927CEF1-A29C-4A8B-A590-7907ED93231F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A927CEF1-A29C-4A8B-A590-7907ED93231F}.Release|Any CPU.Build.0 = Release|Any CPU
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -158,6 +164,7 @@ Global
{C65837A3-1AB1-4AD4-9D9E-C3FF437A5714} = {79268877-BBEF-4DE2-B8D9-697F21933159}
{8FC7B9E5-B651-42BC-B21B-B45F0C8A239B} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04}
{66EF3D1A-32AE-4B28-BC3E-7441615722A9} = {79268877-BBEF-4DE2-B8D9-697F21933159}
{A927CEF1-A29C-4A8B-A590-7907ED93231F} = {79268877-BBEF-4DE2-B8D9-697F21933159}
{A7F1C0E7-F641-4800-98DA-FA7A7B7B76E9} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04}
{3EBD6CA5-CF2C-4350-922E-CEE2AE01445A} = {79268877-BBEF-4DE2-B8D9-697F21933159}
{CDA8EF6E-F678-4FD3-80B0-5C61FC76EE51} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04}
Expand Down
2 changes: 2 additions & 0 deletions src/SmartEnum.JsonNet/SmartEnumNameConverter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;

namespace Ardalis.SmartEnum.JsonNet
{
using Newtonsoft.Json;
Expand Down
56 changes: 56 additions & 0 deletions src/SmartEnum.JsonNet/SmartFlagEnumNameConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Linq;
using System.Text;
using Newtonsoft.Json;

namespace Ardalis.SmartEnum.JsonNet
{
public class SmartFlagEnumNameConverter<TEnum, TValue> : JsonConverter<TEnum>
where TEnum : SmartFlagEnum<TEnum, TValue>
where TValue : struct, IComparable<TValue>, IEquatable<TValue>
{
public override bool CanRead => true;

public override bool CanWrite => true;

public override TEnum ReadJson(JsonReader reader, Type objectType, TEnum existingValue, bool hasExistingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.String:
return GetFromName((string)reader.Value);

default:
throw new JsonSerializationException($"Unexpected token {reader.TokenType} when parsing a smart flag enum.");
}

TEnum GetFromName(string name)
{
try
{
return SmartFlagEnum<TEnum, TValue>.FromName(name, false).FirstOrDefault();
}
catch (Exception ex)
{
throw new JsonSerializationException($"Error converting value '{name}' to a smart flag enum.", ex);
}
}
}

/// <summary>
///
/// </summary>
/// <param name="writer"></param>
/// <param name="value"></param>
/// <param name="serializer"></param>
public override void WriteJson(JsonWriter writer, TEnum value, JsonSerializer serializer)
{
if (value is null)
writer.WriteNull();
else
writer.WriteValue(value.Name);
}
}
}
Loading

0 comments on commit 674d281

Please sign in to comment.