Git Product home page Git Product logo

thinktecture.runtime.extensions's Introduction

Build TestResults NuGet Downloads

Thinktecture.Runtime.Extensions
Thinktecture.Runtime.Extensions.EntityFrameworkCore6
Thinktecture.Runtime.Extensions.EntityFrameworkCore7
Thinktecture.Runtime.Extensions.EntityFrameworkCore8
Thinktecture.Runtime.Extensions.Json
Thinktecture.Runtime.Extensions.Newtonsoft.Json
Thinktecture.Runtime.Extensions.MessagePack
Thinktecture.Runtime.Extensions.AspNetCore

This library provides some interfaces, classes, Roslyn Source Generators, Roslyn Analyzers and Roslyn CodeFixes for implementation of Smart Enums and Value Objects.

Documentation for version 7

See wiki for more documentation.

I recently started writing down some ideas and real-world use cases I used in the past to show the developers the benefits of value objects and smart enums. More examples will come very soon!

Smart Enums:

Value objects:

Requirements

  • Version 7:
    • C# 11 (or higher) for generated code
    • SDK 7.0.401 (or higher) for building projects
  • Version 6:
    • C# 11 (or higher) for generated code
    • SDK 7.0.202 (or higher) for building projects
  • Version 5:
    • C# 9 (or higher) for generated code
    • SDK 6.0.300 (or higher) for building projects

Migrations

Smart Enums

Install: Install-Package Thinktecture.Runtime.Extensions

Documentation: Smart Enums

Features:

Definition of a 2 Smart Enums without any custom properties and methods. All other features mentioned above are generated by the Roslyn Source Generators in the background.

// Smart Enum with a string as the underlying type
[SmartEnum<string>]
public sealed partial class ProductType
{
   public static readonly ProductType Groceries = new("Groceries");
   public static readonly ProductType Housewares = new("Housewares");
}

// Smart Enum with an int as the underlying type
[SmartEnum<int>]
public sealed partial class ProductGroup
{
   public static readonly ProductGroup Apple = new(1);
   public static readonly ProductGroup Orange = new(2);
}

// Smart Enum without identifier (keyless)
[SmartEnum]
public sealed partial class SalesCsvImporterType
{
   public static readonly SalesCsvImporterType Daily = new(articleIdIndex: 0, volumeIndex: 2);
   public static readonly SalesCsvImporterType Monthly = new(articleIdIndex: 2, volumeIndex: 0);

   public int ArticleIdIndex { get; }
   public int VolumeIndex { get; }
}

Behind the scenes a Roslyn Source Generator generates additional code. Some of the features that are now available are ...

// A private constructor which takes the key "Groceries" and additional members (if we had any)
[SmartEnum<string>]
public sealed partial class ProductType
{
   public static readonly ProductType Groceries = new("Groceries");
   ...

------------

// A property for iteration over all items
IReadOnlyList<ProductType> allTypes = ProductType.Items;

------------

// Getting the item with specific name, i.e. its key.
// Throws UnknownEnumIdentifierException if the provided key doesn't belong to any item
ProductType productType = ProductType.Get("Groceries");

// Alternatively, using an explicit cast (behaves the same as "Get")
ProductType productType = (ProductType)"Groceries";

------------

// the same as above but returns a bool instead of throwing an exception (dictionary-style)
bool found = ProductType.TryGet("Groceries", out ProductType? productType);

------------

// similar to TryGet but accepts `IFormatProvider` and returns a ValidationError instead of a boolean.
ValidationError? validationError = ProductType.Validate("Groceries", null, out ProductType? productType);

if (validationError is null)
{
    logger.Information("Product type {Type} found with Validate", productType);
}
else
{
    logger.Warning("Failed to fetch the product type with Validate. Validation error: {validationError}", validationError.ToString());
}

------------

// implicit conversion to the type of the key
string key = ProductType.Groceries; // "Groceries"

------------

// Equality comparison
bool equal = ProductType.Groceries.Equals(ProductType.Groceries);

------------

// Equality comparison with '==' and '!='
bool equal = ProductType.Groceries == ProductType.Groceries;
bool notEqual = ProductType.Groceries != ProductType.Groceries;

------------

// Hash code
int hashCode = ProductType.Groceries.GetHashCode();

------------

// 'ToString' implementation
string key = ProductType.Groceries.ToString(); // "Groceries"

------------

ILogger logger = ...;

// Switch-case (with "Action")
productType.Switch(ProductType.Groceries, () => logger.Information("Switch with Action: Groceries"),
                   ProductType.Housewares, () => logger.Information("Switch with Action: Housewares"));
                   
// Switch-case with parameter (Action<TParam>) to prevent closures
productType.Switch(logger,
                   ProductType.Groceries, static l => l.Information("Switch with Action: Groceries"),
                   ProductType.Housewares, static l => l.Information("Switch with Action: Housewares"));

// Switch case returning a value (Func<TResult>)
var returnValue = productType.Switch(ProductType.Groceries, static () => "Switch with Func<T>: Groceries",
                                     ProductType.Housewares, static () => "Switch with Func<T>: Housewares");

// Switch case with parameter returning a value (Func<TParam, TResult>) to prevent closures
var returnValue = productType.Switch(logger,
                                     ProductType.Groceries, static l => "Switch with Func<T>: Groceries",
                                     ProductType.Housewares, static l => "Switch with Func<T>: Housewares");

// Map an item to another instance
returnValue = productType.Map(ProductType.Groceries, "Map: Groceries",
                              ProductType.Housewares, "Map: Housewares");
------------

// Implements IParsable<T> which is especially helpful with minimal apis.
bool parsed = ProductType.TryParse("Groceries", null, out ProductType? parsedProductType);

------------

// Implements IFormattable if the underlyng type (like int) is an IFormattable itself.
var formatted = ProductGroup.Fruits.ToString("000", CultureInfo.InvariantCulture); // 001

------------

// Implements IComparable and IComparable<T> if the key member type (like int) is an IComparable itself.
var comparison = ProductGroup.Fruits.CompareTo(ProductGroup.Vegetables); // -1

// Implements comparison operators (<,<=,>,>=) if the underlyng type (like int) has comparison operators itself.
var isBigger = ProductGroup.Fruits > ProductGroup.Vegetables;       

Definition of a new Smart Enum with 1 custom property RequiresFoodVendorLicense and 1 method Do with different behaviors for different enum items.

[SmartEnum<string>]
public partial class ProductType
{
   public static readonly ProductType Groceries = new("Groceries",  requiresFoodVendorLicense: true);
   public static readonly ProductType Housewares = new HousewaresProductType();

   public bool RequiresFoodVendorLicense { get; }

   public virtual void Do()
   {
      // do default stuff
   }

   private sealed class HousewaresProductType : ProductType
   {
      public HousewaresProductType()
         : base("Housewares", requiresFoodVendorLicense: false)
      {
      }

      public override void Do()
      {
         // do special stuff
      }
   }
}

Value Objects

Install: Install-Package Thinktecture.Runtime.Extensions

Documentation: Value Objects

Features:

Simple Value Object

A simple value object has 1 field/property only, i.e., it is kind of wrapper for another (primitive) type. The main use case is to prevent creation of values/instances which are considered invalid according to some rules. In DDD (domain-driven design), working with primitive types, like string, directly is called primitive obsession and should be avoided.

Most simple value objects with a key member of type string and another one (which is a struct) with an int.

[ValueObject<string>]
public sealed partial class ProductName
{
}

[ValueObject<int>]
public readonly partial struct Amount
{
}

After the implementation of a value object, a Roslyn source generator kicks in and implements the rest. Following API is available from now on.

// Factory method for creation of new instances.
// Throws ValidationException if the validation fails (if we had any)
ProductName apple = ProductName.Create("Apple");

// Alternatively, using an explicit cast, which behaves the same way as calling "ProductName.Create"
ProductName apple = (ProductName)"Apple";

-----------

// The same as above but returns a bool instead of throwing an exception (dictionary-style)
bool created = ProductName.TryCreate("Chocolate", out ProductName? chocolate);

-----------

// Similar to TryCreate but returns a ValidationError instead of a boolean.
ValidationError? validationError = ProductName.Validate("Chocolate", null, out var chocolate);

if (validationError is null)
{
    logger.Information("Product name {Name} created", chocolate);
}
else
{
    logger.Warning("Failed to create product name. Validation result: {validationError}", validationError.ToString());
}

-----------

// Implicit conversion to the type of the key member
string valueOfTheProductName = apple; // "Apple"

-----------

// Equality comparison compares the key member using default comparer by default.
// Key members of type `string` are compared with 'StringComparer.OrdinalIgnoreCase' by default.
bool equal = apple.Equals(apple);

-----------

// Equality comparison operators: '==' and '!='
bool equal = apple == apple;
bool notEqual = apple != apple;

-----------

// Hash code: combined hash code of type and key member. 
// Strings are using 'StringComparer.OrdinalIgnoreCase' by default.
int hashCode = apple.GetHashCode();

-----------

// 'ToString' implementation return the string representation of the key member
string value = apple.ToString(); // "Apple"

------------

// Implements IParsable<T> which is especially helpful with minimal apis.
bool success = ProductName.TryParse("New product name", null, out var productName);

ProductName productName = ProductName.Parse("New product name", null);

------------

// Implements "IFormattable" if the key member is an "IFormattable".
Amount amount = Amount.Create(42);
string formattedValue = amount.ToString("000", CultureInfo.InvariantCulture); // "042"

------------

// Implements "IComparable<ProductName>" if the key member is an "IComparable",
// or if custom comparer is provided.
Amount amount = Amount.Create(1);
Amount otherAmount = Amount.Create(2);

int comparison = amount.CompareTo(otherAmount); // -1

------------

// Implements comparison operators (<,<=,>,>=) if the key member has comparison operators itself.
bool isBigger = amount > otherAmount;

// Implements comparison operators to compare the value object with an instance of key-member-type,
// if "ComparisonOperators" is set "OperatorsGeneration.DefaultWithKeyTypeOverloads"
bool isBigger = amount > 2;

------------

// Implements addition / subtraction / multiplication / division if the key member supports corresponding operators
Amount sum = amount + otherAmount;

// Implements operators that accept an instance of key-member-type,
// if the "OperatorsGeneration" is set "DefaultWithKeyTypeOverloads"
Amount sum = amount + 2;

------------

// Provides a static default value "Empty" (similar to "Guid.Empty"), if the value object is a struct
Amount defaultValue = Amount.Empty; // same as "Amount defaultValue = default;"

Complex Value Objects

A complex value object is an immutable class or a readonly struct with a ComplexValueObjectAttribute. Complex value object usually has multiple readonly fields/properties.

A simple example would be a Boundary with 2 properties. One property is the lower boundary and the other is the upper boundary. Yet again, we skip the validation at the moment.

[ComplexValueObject]
public sealed partial class Boundary
{
   public decimal Lower { get; }
   public decimal Upper { get; }
}

The rest is implemented by a Roslyn source generator, providing the following API:

// Factory method for creation of new instances.
// Throws ValidationException if the validation fails (if we had any)
Boundary boundary = Boundary.Create(lower: 1, upper: 2);

-----------

// the same as above but returns a bool instead of throwing an exception (dictionary-style)
bool created = Boundary.TryCreate(lower: 1, upper: 2, out Boundary? boundary);

-----------

// similar to TryCreate but returns a ValidationError instead of a boolean.
ValidationError? validationError = Boundary.Validate(lower: 1, upper: 2, out Boundary? boundary);

if (validationError is null)
{
    logger.Information("Boundary {Boundary} created", boundary);
}
else
{
    logger.Warning("Failed to create boundary. Validation result: {validationError}", validationError.ToString());
}

-----------

// Equality comparison compares the members using default or custom comparers.
// Strings are compared with 'StringComparer.OrdinalIgnoreCase' by default.
bool equal = boundary.Equals(boundary);

-----------

// Equality comparison with '==' and '!='
bool equal = boundary == boundary;
bool notEqual = boundary != boundary;

-----------

// Hash code of the members according default or custom comparers
int hashCode = boundary.GetHashCode();

-----------

// 'ToString' implementation
string value = boundary.ToString(); // "{ Lower = 1, Upper = 2 }"

thinktecture.runtime.extensions's People

Contributors

dependabot[bot] avatar pawelgerr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

Forkers

fredatgithub

thinktecture.runtime.extensions's Issues

Remove explicit system.text.json dependency

Is your feature request related to a problem? Please describe.
I want to minimise dependencies in my project by utilising framework dependencies wherever possible

Describe the solution you'd like
I want the package to not have an explicit dependency on System.Text.Json as it can be provided by the framework.

Describe alternatives you've considered
Accept the additional dependency

Additional context
n/a

SmartEnum with static readonly field named System causes error

Hi Pawel,

implementing your SmartEnum with static readonly field named System causes error:

'MySmartEnum' does not contain a definition for 'StringComparer' and no accessible extension method 'StringComparer' accepting a first argument of type 'MySmartEnum' could be found (are you missing a using directive or an assembly reference?)

using Thinktecture;

namespace MyNamespace;

public partial class MySmartEnum : IEnum<string>
{
   public static readonly MySmartEnum System = new("System");
   public static readonly MySmartEnum User = new("User");
}

In the generated source file MySmartEnum_Generated.cs arises a conflict between System.StringComparer and the field named System.

I am using Thinktecture.Runtime.Extensions v4.0.1.

Best regards,
David

SmartEnum causes error

Hi Pawel,

using updated version now causes this error:

'IEnumerable' does not contain a definition for 'ToList' and no accessible extension method 'ToList' accepting a first argument of type 'MySmartEnum' could be found (are you missing a using directive or an assembly reference?)

using Thinktecture;

namespace MyNamespace;

public partial class MySmartEnum : IEnum<string>
{
   public static readonly MySmartEnum MyItem = new("MyItem");
}

See MyNamespace.MySmartEnum.g.cs:

/* file: MyNamespace.MySmartEnum.g.cs */

/// <summary>
/// Gets all valid items.
/// </summary>
public static global::System.Collections.Generic.IReadOnlyList<global::MyNamespace.MySmartEnum> Items => _items ??= ItemsLookup.Values.ToList().AsReadOnly();

Greetings,
David

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.