version | package |
---|---|
Qowaiv | |
Qowaiv.Data.SqlCient | |
Qowaiv.TestTools |
Qowaiv is a (Single) Value Object library. It aims to model reusable (Single) Value Objects that can be used a wide variety of modeling scenarios, both inside and outside a Domain-driven context.
Supported scenarios include parsing, formatting, validation, (de)serialization, model binding, and domain specific logic.
A Value Object that can be represented by a single scalar.
Visual Studio VS2017.3 or higher is required. Visual Studio can be downloaded here: visualstudio.com/downloads.
Represents a date, so without hours (minutes, seconds, milliseconds).
Represents a date span. Opposed to a TimeSpan
its duration is (a bit) resilient;
Adding one month to a date in January result in adding a different number of days,
than adding one month a date in March.
Date spans are particular useful in scenario's for defining (and doing calculations on) month based periods, and ages (mostly in years and days).
var span = new DateSpan(years: 3, months: 2, days: -4);
var age = DateSpan.Age(new Date(2017, 06, 11)); // 2Y+0M+121D on 2019-10-10
var duration = DateSpan.Subtract(new Date(2019, 06, 10), new Date(2017, 06, 11)); // 1Y+11M+30D
var date = new Date(2016, 06, 03).Add(age); // 2018-10-02
Represents an Elo (rating), a method for calculating the relative skill levels of players in competitor-versus-competitor games.
Represents a (single) email address. Supports:
- Display names (are stripped)
- Comments (are removed)
- IP-based domains (normalized and surrounded by brackets)
Furthermore, the email address is normalized as a lowercase string, making it case-insensitive.
var email = EmailAddress.Parse("Test Account <mailto:TEST@qowaiv.org>");
var quoted = EmailAddress.Parse("\"Joe Smith\" email@qowaiv.org");
var ip_based = EmailAddress.Parse("test@[172.16.254.1]");
email.ToString(); // test@qowaiv.org
quoted.ToString(); // email@qowaiv.org
ip_based.IsIPBased; // true
ip_based.WithDisplayName("Jimi Hendrix"); // Jimi Hendrix <test@[172.16.254.1]>
Represents a collection of unique email addresses, excluding the empty and unknown email address.
Represents the size of a file or stream.
Represents a gender based on an ISO 5218 code.
Represents a house number in the range [1-999999999].
Explicitly marked local date time. It allows the clear distinction between local and UTC-based date times.
Represents a month in the range [1-12].
Month feb = Month.Parse("February");
Month may = Month.May;
Month dec = 12;
feb.ToString("f", new CultureInfo("nl-NL")); // februari
feb.ToString("s"); // Feb
feb.ToString("M"); // 02
feb.ToString("m"); // 2
Represents a percentage. It supports parsing from per mile and per ten thousand
too. The basic thought is that Percentage.Parse("14%")
has the same result
as double.Parse("14%")
, which is 0.14
.
// Creation
Percentage p = 0.0314; // implicit cast: 3.14%
var p = Percentage.Parse("3.14"); // Parse: 3.14%;
var p = Percentage.Parse("3.14%"); // Parse: 3.14%;
var p = Percentage.Parse("31.4‰"); // Parse: 3.14%;
var p = 3.14.Percent(); // Extension on double: 3.14%;
// Manipulation
var p = 13.2.Percent();
p++; // 14.2%;
var total = 400;
total *= (Percentage)0.5; // Total = 200;
var value = 50.0;
value += (Percentage)0.1; // value 55;
var rounded = 17.56.Percent().Round(1); // 17.6%;
var max = Percentage.Max(1.4.Percent(), 1.8.Percent()); // 1.8%;
var min = Percentage.Min(1.7.Percent(), 1.9.Percent()); // 1.7%;
Represents a postal code. It supports validation for all countries.
The UUID (Universally unique identifier) aka GUID (Globally unique identifier) is an extension on the System.Guid. It is by default represented by a 22 length string, instead of a 32 length string.
Represents a week based date.
Represents a year in the range [1-9999].
A Yes-no is a (bi-)polar that obviously has the values "yes" and "no". It also
has an "empty"(unset) and "unknown" value. It maps easily with a boolean
, but
Supports all kind of formatting (and both empty and unknown) that can not be
achieved when modeling a property as bool
instead of an YesNo
.
A seed, representing random data to encrypt and decrypt data.
Represents money without the notion of the actual currency.
Represents a BIC as specified in ISO 13616.
var bic = BusinessIdentifierCode.Parse("AEGONL2UXXX");
var business = bic.Business; // "AEGO"
var country = bic.Country; // Country.NL
var location = bic.Location; // "2U"
var branch = bic.Branch; // "XXX"
var length = bic.Length; // 11
Represents a currency based on an ISO 4217 code.
Represents an IBAN as specified in ISO 13616.
var iban = InternationalBankAccountNumber.Parse("nl20ingb0001234567");
iban.Country; // Country.NL
iban.Length; // 18
iban.ToString("F");// NL20 INGB 0001 2345 67
Represents the amount and the currency. Technically this is not SVO. However it acts identically as a SVO.
Money money = 125.34 + Currency.EUR;
var sum = (12 + Currency.EUR) + (15 + Currency.USD); // Throws CurrencyMismatchException()
var rounded = money.Round(0); // EUR 125.00
Represents a country based on an ISO 3166-1 code (or 3166-3 if the country does not longer exists).
Represents a (MS SQL) time-stamp is a data type that exposes automatically generated binary numbers, which are guaranteed to be unique within a database. time-stamp is used typically as a mechanism for version-stamping table rows. The storage size is 8 bytes. See: https://technet.microsoft.com/en-us/library/aa260631%28v=sql.80%29.aspx
To create a (SQL) parameter with a SVO as value, use the SvoParamater factory class. It will return SQL parameter with a converted database proof value.
Represents an Internet media type (also known as MIME-type and content type).
Represents a pattern to match strings, using wildcard characters ? and *. It also support the use of SQL wildcard characters _ and %.
By default, .NET support rounding of floating points (including decimal
s).
However, for some domains this support is too limited. To overcome this, Qowaiv
has the static DecimalRound
helper class, containing extension methods for rounding.
To round tenfold, hundredfold, etc. precision, a negative amount of decimals can be specified:
var tenfold = 1245.346m.Round(-1); // 1250m
var hundredfold = 1209m.Round(-2); // 1200m
Rounding to a multiple of is supported:
var multipleOf = 123.5m.RoundToMultiple(5m); // 125.0m
var multiple25 = 123.5m.RoundToMultiple(2.5m); // 122.5m
.NET supports rounding to even (Bankers rounding) and away from zero out-of-the-box.
Rounding methods like ceiling, floor, and truncate have limited support (0 decimals only),
and many others (to odd, half-way up, half-way down, e.o.) are missing.
By specifying the DecimalRounding
13 ways are supported.
var toOdd = 23.0455m.Round(3, DecimalRounding.ToOdd); // 23.045m
var towardsZero = 23.5m.Round(DecimalRounding.TowardsZero); // 23m
var randomTie = 23.5m.Round(DecimalRounding.RandomTieBreaking); // 50% 23m, 50% 24,
All SVO's support model binding out of the box. That is to say, when the model
binding mechanism works with a TypeConverter
. It still may be beneficial to
have a custom model binder. Because different solutions might require different
custom model binders, and deploying them as NuGet packages would potentially
lead to a dependency hell, Qowaiv provides them as code snippets:
Serializing data using JSON is de facto the default. Qowaiv has a (naming) based convention:
public struct Svo
{
public static Svo FromJson(string json);
// When appropriate for the SVO. Example: `Percentage`.
public static Svo FromJson(double json);
// When appropriate for the SVO. Example: `Amount`.
public static Svo FromJson(long json);
// When appropriate for the SVO. Example: `YesNo`.
public static Svo FromJson(bool json);
// In most cases `string` is returned, but there are exceptions:
// Amount: double ToJson();
// StreamSize: long ToJson();
// Year: object ToJson();
public object /* or string, bool, int, long, double, decimal */ ToJson();
}
There are two out-of-the-box implementations that that support this convention based contract.
The OpenAPI Specification (formerly Swagger Specification) is an API description format for REST API's.
To improve usage of your REST API's you should specify the Data Type of your
SVO's. To make this as simple as possible, Qowaiv SVO's are decorated with the
OpenApiDataTypeAttribute
. It specifies the type, format, (regex) pattern,
and if the data type is nullable, all when applicable.
{
"Date": {
"description": "Full-date notation as defined by RFC 3339, section 5.6, for example, 2017-06-10.",
"type": "string",
"format": "date",
"nullabe": false
},
"DateSpan": {
"description": "Date span, specified in years, months and days, for example 1Y+10M+16D.",
"type": "string",
"format": "date-span",
"pattern": "[+-]?[0-9]+Y[+-][0-9]+M[+-][0-9]+D",
"nullabe": false
},
"EmailAddress": {
"description": "Email notation as defined by RFC 5322, for example, svo@qowaiv.org.",
"type": "string",
"format": "email",
"nullabe": true
},
"EmailAddressCollection": {
"description": "Comma separated list of email addresses defined by RFC 5322.",
"type": "string",
"format": "email-collection",
"nullabe": true
},
"Gender": {
"description": "Gender as specified by ISO/IEC 5218.",
"type": "string",
"format": "gender",
"nullabe": true,
"enum": [
"NotKnown",
"Male",
"Female",
"NotApplicable"
]
},
"HouseNumber": {
"description": "House number notation.",
"type": "string",
"format": "house-number",
"nullabe": true
},
"LocalDateTime": {
"description": "Date-time notation as defined by RFC 3339, without time zone information, for example, 2017-06-10 15:00.",
"type": "string",
"format": "local-date-time",
"nullabe": false
},
"Month": {
"description": "Month(-only) notation.",
"type": "string",
"format": "month",
"nullabe": true,
"enum": [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
"?"
]
},
"Percentage": {
"description": "Ratio expressed as a fraction of 100 denoted using the percent sign '%', for example 13.76%.",
"type": "string",
"format": "percentage",
"pattern": "-?[0-9]+(\\.[0-9])?%",
"nullabe": false
},
"PostalCode": {
"description": "Postal code notation.",
"type": "string",
"format": "postal-code",
"nullabe": true
},
"Uuid": {
"description": "Universally unique identifier, Base64 encoded, for example lmZO_haEOTCwGsCcbIZFFg.",
"type": "string",
"format": "uuid-base64",
"nullabe": true
},
"WeekDate": {
"description": "Full-date notation as defined by ISO 8601, for example, 1997-W14-6.",
"type": "string",
"format": "date-weekbased",
"nullabe": false
},
"Year": {
"description": "Year(-only) notation.",
"type": "integer",
"format": "year",
"nullabe": true
},
"YesNo": {
"description": "Yes-No notation.",
"type": "string",
"format": "yes-no",
"nullabe": true,
"enum": [
"yes",
"no",
"?"
]
},
"Financial.Amount": {
"description": "Decimal representation of a currency amount.",
"type": "number",
"format": "amount",
"nullabe": false
},
"Financial.BusinessIdentifierCode": {
"description": "Business Identifier Code, as defined by ISO 9362, for example, DEUTDEFF.",
"type": "string",
"format": "bic",
"nullabe": true
},
"Financial.Currency": {
"description": "Currency notation as defined by ISO 4217, for example, EUR.",
"type": "string",
"format": "currency",
"nullabe": true
},
"Financial.InternationalBankAccountNumber": {
"description": "International Bank Account Number notation as defined by ISO 13616:2007, for example, BE71096123456769.",
"type": "string",
"format": "iban",
"nullabe": true
},
"Financial.Money": {
"description": "Combined currency and amount notation as defined by ISO 4217, for example, EUR 12.47.",
"type": "string",
"format": "money",
"pattern": "[A-Z]{3} -?[0-9]+(\\.[0-9]+)?",
"nullabe": false
},
"Globalization.Country": {
"description": "Country notation as defined by ISO 3166-1 alpha-2, for example, NL.",
"type": "string",
"format": "country",
"nullabe": true
},
"IO.StreamSize": {
"description": "Stream size notation (in byte).",
"type": "integer",
"format": "stream-size",
"nullabe": false
},
"Security.Cryptography.CryptographicSeed": {
"description": "Base64 encoded cryptographic seed.",
"type": "string",
"format": "cryptographic-seed",
"nullabe": true
},
"Statistics.Elo": {
"description": "Elo rating system notation.",
"type": "number",
"format": "elo",
"nullabe": false
},
"Web.InternetMediaType": {
"description": "Media type notation as defined by RFC 6838, for example, text/html.",
"type": "string",
"format": "internet-media-type",
"nullabe": true
}
}
When using Swagger to implement OpenApi this could be done like below:
/// <summary>Extensions on <see cref="SwaggerGenOptions"/>.</summary>
public static class SwaggerGenOptionsSvoExtensions
{
/// <summary>Maps Qowaiv SVO's.</summary>
public static SwaggerGenOptions MapSingleValueObjects(this SwaggerGenOptions options)
{
var attributes = OpenApiDataTypeAttribute.From(typeof(Date).Assembly);
foreach (var attr in attributes)
{
options.MapType(attr.DataType, () => new OpenApiSchema
{
Type = attr.Type,
Format = attr.Format,
Pattern = attr.Pattern,
Nullable = attr.Nullable,
});
}
}
}
.NET supports XML Serialization out-of-the-box. All SVO's implement IXmlSerialization
with the same approach:
XmlSchema IXmlSerializable.GetSchema() => null;
void IXmlSerializable.ReadXml(XmlReader reader)
{
var s = reader.ReadElementString();
var val = Parse(s, CultureInfo.InvariantCulture);
m_Value = val.m_Value;
}
void IXmlSerializable.WriteXml(XmlWriter writer)
{
writer.WriteString(ToString(SerializableFormat, CultureInfo.InvariantCulture));
}
To support hashing (object.GetHashCode()
) the hash code should always return
the same value, for the same object. As SVO's are equal by value, the hash
is calculated based on the underlying value.
Due to IXmlSerialization support, however, the underlying value is not
read-only, because this interface first create default instance and then
sets the value. Only if somebody intentionally misuses the IXmlSerialization
interface, can change a value during the lifetime of a SVO.
Therefor
#pragma warning disable S2328
// "GetHashCode" should not reference mutable fields
// See README.md => Hashing
is fine.
SVO's support sorting. So, LINQ expressions like OrderBy() and OrderByDescending() work out of the box, just like Array.Sort(), and List.Sort(). However, the comparison operators (<, >, <=, >=) do only make sense for a subset of those, and are not implemented on all.
Therefor
#pragma warning disable S1210
// "Equals" and the comparison operators should be overridden when implementing "IComparable"
// See README.md => Sortable
is fine for types that are sortable via IComparable (in most cases).
During debugging sessions, by default, the IDE shows the result of ToString() on a watch. Although Tostring() is overridden for all Qowaiv Single Value Objects, for debugging a special debugger display is provided too, using a debugger display attribute.
The debugger display attribute refers to (private) property with the name "DebuggerDisplay", which represents the Single Value Object as string. If supported, formatted, and in case of a Empty or Unknown value with a notification of that too. The outcome of the DebuggerDisplay is tested in the UnitTests.
Because the rendering of debugger display is handled based on the development environment, and methods as debugger display are not supported by VB.NET, the debugger display attribute refers to a property instead.
Formatting is an important part of the functionality in Qowaiv. All SVO's implement IFormattable, and have custom formatting. For details, see the different remarks at the ToString(string, IFormatProvider).
The formatting arguments object, is a container object (struct) of the format and the format provider, the two arguments required for the System.Iformatable ToString() method.
This collection of formatting arguments stores them based on a type to apply on. On top of that, it has a Format() method, that is an extended implementation of string.Format(). The difference between these two methods is, that - when no custom format is supplied at the format string - string.Format() the default formatting of the object is used, where FormattingArgumentsCollection.Format() uses the default specified at the formatting collection of a type (if available).
Because there are scenario's where you want to set typical values as a country or a currency for the context of the current thread (like the culture info) there is a possibility to add these to the Qowaiv.Threading.ThreadDomain.
These values can be configured (in the application settings) or can be created with a creator function that can be registered. If not specified otherwise the current country will be created (if possible) based on the current culture.
The Clock
class is an outsider within the Qowaiv library. It is a solution
for a problem that is not related to Domain-Driven Design, but to the fact that
the behaviour of System.DateTime.UtcNow
(and its equivalents) can not be controlled.
This can be problematic for writing proper tests that relay on its behaviour.
The default way to tackle this problem is by providing a lightweight service like this one:
public interface IClock
{
DateTime UtcNow();
}
public class Clock : IClock
{
public DateTime UtcNow() => DateTime.UtcNow;
}
However, providing an IClock all the time when there is time related logic is
not that elegant at all. The Qowaiv Clock
helps to overcome this. In code
you just call Clock.UtcNow()
or one of its derived methods. In a test you
change the behaviour, in most cases just for the scope of your current threat:
[Test]
public void TestSomething()
{
using(Clock.SetTimeForCurrentThread(() => new DateTime(2017, 06, 11))
{
// test code.
}
}