Upgrading to MicroLite 6.2

MicroLite 6.2.0 has been released on NuGet.

The 2 main changes in this release are:

  • The session re-uses the same command during its lifecycle so multiple operations via a session are more efficient – especially multiple operations of the same type (e.g. multiple inserts).
  • Improved the handling of TimeSpan – beware that this is a breaking change (see below)

Prior to MicroLite 6.2, MicroLite was able to map a TimeSpan to a MS SQL Time column – however it turns out there are a number of problems with the Time type:

  1. It has a maximum value of 24:00:00 since it is actually designed to represent the time of day
  2. It cannot be used in some aggregate functions such as SUM and AVERAGE which makes summarising data diffucult

In order to mitigate these issues, MicroLite 6.2 now maps TimeSpan to a BIGINT by persisting the tick count of the TimeSpan – this has the side effect of also enabling TimeSpan to be used by any database supported by MicroLite.

If you are updating to MicroLite 6.2 and you are currently using TimeSpan -> TIME you have 2 options:

1) – Update your database schema to use a BIGINT (add a new column, populate it by casting the time as the tick count, remove the old column).

2) – Configure MicroLite 6.2 to behave like MicroLite 6.1:

// In startup code (before calling Configure.Fluently()...):

// 1. Remove the new TimeSpanTypeConverter:
var timeSpanTypeConverter = TypeConverter.Converters.OfType<TimeSpanTypeConverter>().Single();
TypeConverter.Converters.Remove(timeSpanTypeConverter);

// 2. Reset the DbType mapping for TimeSpan:
TypeConverter.RegisterTypeMapping(typeof(TimeSpan), System.Data.DbType.Time);
TypeConverter.RegisterTypeMapping(typeof(TimeSpan?), System.Data.DbType.Time);

Understanding InvalidCastException

We can sometimes get an InvalidCastException thrown when reading a record from the database, the usual reason for this is that the database column does not match the property type on the class. For example, we have a column defined in the database as:

PhoneNumber int not null

and in the class we have

public string PhoneNumber { get; set; }

The reason this will fail is that MicroLite will read the value using the most efficient method and since the property is a string and IDataReader has a method GetString that will be called, essentially doing this:

obj.PhoneNumber = dataReader.GetString(idx);

However since the actual returned value in the IDataReader is an int which cannot be implicitly cast to a string we get an InvalidCastException. To resolve this, change the property type and database column to be compatible types (e.g. change the database column to string or change the class property to int).

MicroLite 6.0 Logging/Debugging

One of the things we focused on in MicroLite 6.0 is making the logging and debugging process even nicer for developers.

Since very early on MicroLite has had support for writing to your application log via the logging extension packages. However it has never been easy to see how a class has been mapped. To make this easier to understand, we have created an extension method for IObjectInfo which can be used to write the mapping to a TextWriter. We also write this to the log if the log is capturing Debug statements.

It’s as easy as this:

// Resolve the IObjectInfo for the mapped class.
var objectInfo = ObjectInfo.For(typeof(Customer));

// Write the mappings to the console
objectInfo.EmitMappingsToConsole();
// or write the mappings to a string
var stringWriter = new StringWriter()

// Write mappings to a TextWriter
objectInfo.EmitMappings(stringWriter);
var mappingDetails = stringWriter.ToString();

For the following class using the default conventions:

public class Customer
{
    public int Id { get; set; }
    public string Forename { get; set; }
    public string Surname { get; set; }
    public DateTime DateOfBirth { get; set; }
    public CustomerStatus Status { get; set; }
}

It will emit the following:

MicroLite Mapping:
——————
Class ‘MyApplication.Customer’ mapped to Table ‘Customers’

Property ‘DateOfBirth (System.DateTime)’ mapped to Column ‘DateOfBirth (DbType.DateTime)’
Allow Insert: True
Allow Update: True
Is Identifier: False

Property ‘Forename (System.String)’ mapped to Column ‘Forename (DbType.String)’
Allow Insert: True
Allow Update: True
Is Identifier: False

Property ‘Id (System.Int32)’ mapped to Column ‘Id (DbType.Int32)’
Allow Insert: False
Allow Update: False
Is Identifier: True
Identifier Strategy: DbGenerated

Property ‘Status (ConsoleApplication5.CustomerStatus)’ mapped to Column ‘CustomerStatusId (DbType.Int32)’
Allow Insert: True
Allow Update: True
Is Identifier: False

Property ‘Surname (System.String)’ mapped to Column ‘Surname (DbType.String)’
Allow Insert: True
Allow Update: True
Is Identifier: False

MicroLite OData update

The latest release of MicroLite.Extensions.WebApi (6.1) and Net.Http.WebApi.OData (3.1) now provide support for nearly the entire OData 3 spec that can be supported by MicroLite.

It is now possible to compare 2 properties:

$filter=FirstName eq LastName

And call nested functions:

$filter=concat(concat(City, ', '), Country) eq 'Berlin, Germany'

Decimal/Double and Single values don’t require .0 for whole values:

$filter=Price eq 5M can now be used as well as $filter=Price eq 5.0M

Method parameters no longer require a space after the comma:

$filter=endswith(CompanyName,'Futterkiste’) can now be used as well as
$filter=endswith(CompanyName, 'Futterkiste')

Literal strings (text wrapped in single quotes) can now contain a single quote by escaping it with another single quote:

$filter=LastName eq ‘O’’Brien’

Grouping is now supported in queries:

$filter=LastName eq 'Smith' and (Title eq 'Mr' or Title eq 'Mrs')

Without the grouping, the query would be translated as:

LastName = Smith and Title = Mr
Or
Title = Mrs

There is no concept of an “in list” in OData so a grouped set of “or”s is the way to achieve the same result.

endswith, startswith, and substringof now no longer require eq true at the end:

startswith(CompanyName,’Alfr’) is the same as startswith(CompanyName,’Alfr’) eq true

The not operator has also been implemented which means you can negate function calls:

not startswith(CompanyName,’Alfr’)

MicroLite.Logging.Serilog

Since very early on MicroLite has had support for writing to your application log via the MicroLite.Logging.Log4Net or MicroLite.Logging.NLog.

With the release of MicroLite 6.1 we have added support for Serilog via the MicroLite.Logging.Serilog package.

You can see the details of how to configure it on the GitHub site.

Please note that whilst this will enable MicroLite to write to your log, it doesn’t support the usual Serilog format log messages so the statements will be formatted strings.

For example, you would see

Beginning a new Transaction with the IsolationLevel 'ReadCommitted'

In the log rather than

Beginning a new Transaction with the IsolationLevel '{isolationLevel}'

This is because MicroLite does not know which if any logging library it will be writing to at runtime so it has to use standard .NET format strings for its log messages.

Upgrading to MicroLite 6.1

There are a couple of changes in MicroLite 6.1 which you need to be aware of when updating from a previous version.

Listeners

In MicroLite 6.1, we have split the IListener interface into 3:

IDeleteListener containing the following methods:

void AfterDelete(object instance, int rowsAffected);
void BeforeDelete(object instance);

IInsertListener containing the following methods:

void AfterInsert(object instance, object executeScalarResult);
void BeforeInsert(object instance);

IUpdateListener containing the following methods:

void AfterUpdate(object instance, int rowsAffected);
void BeforeUpdate(object instance);

You can create as many listeners as you like implementing all or some of the above interfaces and register them in the appropriate collections.

Example – Create a listener which only cares about insert operations:

public class InsertOnlyListener : IInsertListener
{
   public void AfterInsert(object instance, int rowsAffected) { ... }
   public void BeforeInsert(object instance) { ... }
}

Listener.InsertListeners.Add(new MyInsertListener());

Example – Create a listener which cares about insert and update operations:

public class AuditListener : IInsertListener, IUpdateListener
{
   public void AfterInsert(object instance, int rowsAffected) { ... }
   public void AfterUpdate(object instance, int rowsAffected) { ... }
   public void BeforeInsert(object instance) { ... }
   public void BeforeUpdate(object instance) { ... }
}

var auditListener = new AuditListener();
Listener.InsertListeners.Add(auditListener);
Listener.UpdateListeners.Add(auditListener);

SQL Builder

The “write” builders (SqlBuilder.Delete and SqlBuilder.Update) now offer the same functionality for single columns as the read builder (SqlBuilder.Select):

  • Between(object lower, object upper);
  • In(params object[] args);
  • In(params SqlQuery[] subQueries);
  • In(SqlQuery subQuery);
  • IsEqualTo(object comparisonValue);
  • IsGreaterThan(object comparisonValue);
  • IsGreaterThanOrEqualTo(object comparisonValue);
  • IsLessThan(object comparisonValue);
  • IsLessThanOrEqualTo(object comparisonValue);
  • IsLike(object comparisonValue);
  • IsNotEqualTo(object comparisonValue);
  • IsNotLike(object comparisonValue);
  • IsNotNull();
  • IsNull();
  • NotBetween(object lower, object upper);
  • NotIn(params object[] args);
  • NotIn(params SqlQuery[] subQueries);
  • NotIn(SqlQuery subQuery);

MicroLite 6.0 – Specifying the DbType

In MicroLite 6.0, we have exposed the ability to control the mapping between a .NET Type and the Database Type (DbType).

If not explicitly configured, the types will be mapped using the default mappings – you can see the default mappings here

If you use the Attribute based mapping and want to specify the DbType for a specific property, you can set it in the ColumnAttribute applied to that property:

[Column("DoB", DbType.DateTime2)]
public DateTime DateOfBirth { get; set; }

If you use convention based mapping and want to specify the DbType for a specific property, you can define that in your own convention:

        new ConventionMappingSettings
        {
            ResolveDbType = (PropertyInfo propertyInfo) =>
            {
                if (propertyInfo.PropertyType == typeof(DateTime) && propertyInfo.ReflectedType == typeof(Customer))
                {
                     return DbType.DateTime2;
                }

                // Use the default
                return TypeConverter.ResolveDbType(propertyInfo.PropertyType);
            }

If you always want to use a specific DbType for a .NET Type, you can set it via the TypeConverter class:

// Always use DbType.DateTime2 instead of DbType.DateTime for System.DateTime
TypeConverter.RegisterTypeMapping(typeof(DateTime), DbType.DateTime2);