Kros.KORM is simple, fast and easy to use micro-ORM framework for .NETStandard created by Kros a.s. from Slovakia.
- You can easily create query builder for creating queries returning IEnumerable of your POCO objects
- Linq support
- Saving changes to your data (Insert / Update / Delete)
- Kros.KORM supports bulk operations for fast inserting and updating large amounts of data (BulkInsert, BulkDelete)
For configuration, general information and examples see the documentation.
Kros.KORM is available from Nuget Kros.KORM.MsAccess.
To contribute with new topics/information or make changes, see contributing for instructions and guidelines.
- Query
- Linq to Kros.KORM
- DataAnnotation attributes
- Convention model mapper
- Converters
- OnAfterMaterialize
- Property injection
- Model builder
- Committing of changes
- SQL commands executing
- Logging
- Supported database types
- ASP.NET Core extensions
- Unit and performance tests
You can use Kros.KORM for creating queries and their materialization. Kros.KORM helps you put together desired query, that can return instances of objects populated from database by using foreach or linq.
var people = database.Query<Person>()
.Select("p.Id", "FirstName", "LastName", "PostCode")
.From("Person JOIN Address ON (Person.AddressId = Address.Id)")
.Where("Age > @1", 18);
foreach (var person in people)
{
Console.WriteLine(person.FirstName);
}
For more information take a look at definition of IQuery.
Kros.KORM allows you to use Linq for creating queries. Basic queries are translated to SQL language.
var people = database.Query<Person>()
.From("Person JOIN Address ON (Person.AddressId = Address.Id)")
.Where(p => p.LastName.EndsWith("ová"))
.OrderByDescending(p => p.Id)
.Take(5);
foreach (var person in people)
{
Console.WriteLine(person.FirstName);
}
Supported Linq methods are Where, FirstOrDefault, Take, Sum, Max, Min, OrderBy, OrderByDescending, ThenBy, ThenByDescending, Count, Any, Skip.
Other methods, such as Select, GroupBy, Join
are not supported at this moment because of their complexity.
You can use also some string functions in Linq queries:
String function | Example | Translation to T-SQL |
---|---|---|
StartWith | Where(p => p.FirstName.StartWith("Mi")) | WHERE (FirstName LIKE @1 + '%') |
EndWith | Where(p => p.LastName.EndWith("ová")) | WHERE (LastName LIKE '%' + @1) |
Contains | Where(p => p.LastName.Contains("ia")) | WHERE (LastName LIKE '%' + @1 + '%') |
IsNullOrEmpty | Where(p => String.IsNullOrEmpty(p.LastName)) | WHERE (LastName IS NULL OR LastName = '') |
ToUpper | Where(p => p.LastName.ToUpper() == "Smith") | WHERE (UPPER(LastName) = @1) |
ToLower | Where(p => p.LastName.ToLower() == "Smith") | WHERE (LOWER(LastName) = @1) |
Replace | Where(p => p.FirstName.Replace("hn", "zo") == "Jozo") | WHERE (REPLACE(FirstName, @1, @2) = @3) |
Substring | Where(p => p.FirstName.Substring(1, 2) == "oh") | WHERE (SUBSTRING(FirstName, @1 + 1, @2) = @3) |
Trim | Where(p => p.FirstName.Trim() == "John") | WHERE (RTRIM(LTRIM(FirstName)) = @1) |
Translation is provided by implementation of ISqlExpressionVisitor.
Properties (not readonly or writeonly properties) are implicitly mapped to database fields with same name. When you want to map property to database field with different name use AliasAttribute. The same works for mapping POCO classes with database tables.
[Alias("Workers")]
private class Staff
{
[Alias("PK")]
public int Id { get; set; }
[Alias("Name")]
public string FirstName { get; set; }
[Alias("SecondName")]
public string LastName { get; set; }
}
private void StaffExample()
{
using (var database = new Database(_connection))
{
_command.CommandText = "SELECT PK, Name, SecondName from Workers";
using (var reader = _command.ExecuteReader())
{
var staff = database.ModelBuilder.Materialize<Staff>(reader);
}
}
}
When you need to have read-write properties independent of the database use NoMapAttribute
.
[NoMap]
public int Computed { get; set; }
If you have different conventions for naming properties in POCO classes and fields in database, you can redefine behaviour of ModelMapper, which serves mapping POCO classes to database tables and vice versa.
Database.DefaultModelMapper.MapColumnName = (colInfo, modelType) =>
{
return string.Format("COL_{0}", colInfo.PropertyInfo.Name.ToUpper());
};
Database.DefaultModelMapper.MapTableName = (tInfo, type) =>
{
return string.Format("TABLE_{0}", type.Name.ToUpper());
};
using (var database = new Database(_connection))
{
_command.CommandText = "SELECT COL_ID, COL_FIRSTNAME from TABLE_WORKERS";
using (var reader = _command.ExecuteReader())
{
var people = database.ModelBuilder.Materialize<Person>(reader);
foreach (var person in people)
{
Console.WriteLine(person.FirstName);
}
}
}
Alternatively you can write your own implementation of IModelMapper.
Database.DefaultModelMapper = new CustomModelMapper();
If your POCO class is defined in external library, you can redefine mapper, so it can map properties of the model to desired database names.
var externalPersonMap = new Dictionary<string, string>() {
{ nameOf(ExternalPerson.oId), "Id" },
{ nameOf(ExternalPerson.Name), "FirstName" },
{ nameOf(ExternalPerson.SecondName), "LastName" }
};
Database.DefaultModelMapper.MapColumnName = (colInfo, modelType) =>
{
if (modelType == typeof(ExternalPerson))
{
return externalPersonMap[colInfo.PropertyInfo.Name];
}
else
{
return colInfo.PropertyInfo.Name;
}
};
using (var database = new Database(_connection))
{
var people = database.Query<ExternalPerson>();
foreach (var person in people)
{
Console.WriteLine($"{person.oId} : {person.Name}-{person.SecondName}");
}
}
For dynamic mapping you can use method SetColumnName<TModel, TValue>
Database.DefaultModelMapper.SetColumnName<Person, string>(p => p.Name, "FirstName");
Data type of column in database and data type of property in your POCO class may differ. Some of these differences are automatically solved by Kros.KORM, for example double
in database is converted to int
in your model, same as int
in database to enum
in model, etc.
For more complicated conversion Kros.KORM offers possibility similar to data binding in WPF, where IValueConverter
is used.
Imagine you store a list of addresses separated by some special character (for example #) in one long text column, but the property in your POCO class is list of strings.
Let's define a converter that can convert string to list of strings.
public class AddressesConverter : IConverter
{
public object Convert(object value)
{
var ret = new List<string>();
if (value != null)
{
var address = value.ToString();
var addresses = address.Split('#');
ret.AddRange(addresses);
}
return ret;
}
public object ConvertBack(object value)
{
var addresses = string.Join("#", (value as List<string>));
return addresses;
}
}
And now you can set this converter for your property.
[Converter(typeof(AddressesConverter))]
public List<string> Addresses { get; set; }
If you want to do some special action right after materialisation is done (for example to do some calculations) or you want to get some other values from source reader, that can not by processed automatically, your class should implement interface IMaterialize.
You can do whatever you need in method OnAfterMaterialize
.
For example, if you have three int columns for date in database (Year, Month and Day) but in your POCO class you have only one date property, you can solve it as follows:
[NoMap]
public DateTime Date { get; set; }
public void OnAfterMaterialize(IDataRecord source)
{
var year = source.GetInt32(source.GetOrdinal("Year"));
var month = source.GetInt32(source.GetOrdinal("Month"));
var day = source.GetInt32(source.GetOrdinal("Day"));
this.Date = new DateTime(year, month, day);
}
Sometimes you might need to inject some service to your model, for example calculator or logger. For these purposes Kros.KORM offers IInjectionConfigurator
, that can help you with injection configuration.
Let's have properties in model
[NoMap]
public ICalculationService CalculationService { get; set; }
[NoMap]
public ILogger Logger { get; set; }
And that is how you can configure them.
Database.DefaultModelMapper
.InjectionConfigurator<Person>()
.FillProperty(p => p.CalculationService, () => new CalculationService())
.FillProperty(p => p.Logger, () => ServiceContainer.Instance.Resolve<ILogger>());
For materialisation Kros.KORM uses IModelFactory
, that creates factory for creating and filling your POCO objects.
By default DynamicMethodModelFactory
is implemented, which uses dynamic method for creating delegates.
If you want to try some other implementation (for example based on reflexion) you can redefine property Database.DefaultModelFactory
.
Database.DefaultModelFactory = new ReflectionModelfactory();
You can use Kros.KORM also for editing, adding or deleting records from database. IdDbSet is designed for that.
Records to edit or delete are identified by the primary key. You can set primary key to your POCO class by using Key
attribute.
[Key()]
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public void Insert()
{
using (var database = new Database(_connection))
{
var people = database.Query<Person>().AsDbSet();
people.Add(new Person() { Id = 1, FirstName = "Jean Claude", LastName = "Van Damme" });
people.Add(new Person() { Id = 2, FirstName = "Sylvester", LastName = "Stallone" });
people.CommitChanges();
}
}
Kros.KORM supports bulk inserting, which is one of its best features. You add records to DbSet standardly by method Add
, but for committing to database use method BulkInsert
instead of CommitChanges
.
var people = database.Query<Person>().AsDbSet();
foreach (var person in dataForImport)
{
people.Add(person);
}
people.BulkInsert();
Kros.KORM supports also bulk update of records, you can use BulkUpdate
method.
var people = database.Query<Person>().AsDbSet();
foreach (var person in dataForUpdate)
{
people.Edit(person);
}
people.BulkUpdate();
This bulk way of inserting or updating data is several times faster than standard inserts or updates.
For both of bulk operations you can provide data as an argument of method. The advantage is that if you have a specific enumerator, you do not need to spill data into memory.
Kros.KORM supports generating of primary keys for inserted records. Primary key must be simple Int32
column. Primary key property in POCO class must be decorated by Key
attribute and its property AutoIncrementMethodType
must be set to Custom
.
[Key(autoIncrementMethodType: AutoIncrementMethodType.Custom)]
public int Id { get; set; }
Kros.KORM generates primary key for every inserted record, that does not have value for primary key property. For generating primary keys implementations of IIdGenerator are used.
public void Edit()
{
using (var database = new Database(_connection))
{
var people = database.Query<Person>().AsDbSet();
foreach (var person in people)
{
person.LastName += "ová";
people.Edit(person);
}
people.CommitChanges();
}
}
public void Delete()
{
using (var database = new Database(_connection))
{
var people = database.Query<Person>().AsDbSet();
people.Delete(people.FirstOrDefault(x => x.Id == 1));
people.Delete(people.FirstOrDefault(x => x.Id == 2));
people.CommitChanges();
}
}
By default, changes of a DbSet
are committed to database in a transaction. If committing of one record fails, rollback of transaction is executed.
Sometimes you might come to situation, when such implicit transaction would not meet your requirements. For example you need to commit changes to two tables as an atomic operation. When saving changes to first of tables is not successful, you want to discard changes to the other table. Solution of that task is easy with explicit transactions supported by Kros.KORM. See the documentation of BeginTransaction.
using (var transaction = database.BeginTransaction())
{
var invoicesDbSet = database.Query<Invoice>().AsDbSet();
var itemsDbSet = database.Query<Item>().AsDbSet();
try
{
invoicesDbSet.Add(invoices);
invoicesDbSet.CommitChanges();
itemsDbSet.Add(items);
itemsDbSet.CommitChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
}
}
Kros.KORM supports SQL commands execution. There are three types of commands:
ExecuteNonQuery
for commands that do not return value (DELETE, UPDATE, ...)ExecuteScalar
for commands that return only one value (SELECT)ExecuteStoredProcedure
for executing of stored procedures. Stored procedure may return scalar value or list of values or it can return data in output parameters.
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BDay { get; set; }
}
private Database _database = new Database(new SqlConnection("connection string"));
// Stored procedure returns a scalar value.
int intResult = _database.ExecuteStoredProcedure<int>("ProcedureName");
DateTime dateResult = _database.ExecuteStoredProcedure<DateTime>("ProcedureName");
// Stored procedure sets the value of output parameter.
var parameters = new CommandParameterCollection();
parameters.Add("@param1", 10);
parameters.Add("@param2", DateTime.Now);
parameters.Add("@outputParam", null, DbType.String, ParameterDirection.Output);
_database.ExecuteStoredProcedure<string>("ProcedureName", parameters);
Console.WriteLine(parameters["@outputParam"].Value);
// Stored procedure returns complex object.
Person person = _database.ExecuteStoredProcedure<Person>("ProcedureName");
// Stored procedure returns list of complex objects.
IEnumerable<Person> persons = _database.ExecuteStoredProcedure<IEnumerable<Person>>("ProcedureName");
If you want to execute time-consuming command, you will definitely appreciate CommandTimeout
property of transaction. See the documentation of BeginTransaction.
Warning: You can set CommandTimeout
only for main transaction, not for nested transactions. In that case CommandTimout of main transaction will be used.
IEnumerable<Person> persons = null;
using (var transaction = database.BeginTransaction(IsolationLevel.Chaos))
{
transaction.CommandTimeout = 150;
try
{
persons = database.ExecuteStoredProcedure<IEnumerable<Person>>("LongRunningProcedure_GetPersons");
transaction.Commit();
}
catch
{
transaction.Rollback();
}
}
Kros.KORM offers the ability to log each generated and executed query. All you have to do is add this line to your source code.
Database.Log = Console.WriteLine;
Kros.KORM uses its own QueryProvider to execute query in a database. ISqlExpressionVisitor transforms IQuery to SELECT command specific for each supported database engine.
MsAccess is suported from version 2.4 in Kros.KORM.MsAccess library. If you need to work with MS Access database, you have to refer this library in your project and register MsAccessQueryProviderFactory.
MsAccessQueryProviderFactory.Register();
Current version of Kros.KORM suports databases MS ACCESS and MS SQL.
If you want to support a different database engine, you can implement your own IQueryProvider. And register it in QueryProviderFactories.
public class CustomQueryProvider : QueryProvider
{
public CustomQueryProvider(ConnectionStringSettings connectionString,
ISqlExpressionVisitor sqlGenerator,
IModelBuilder modelBuilder,
ILogger logger)
: base(connectionString, sqlGenerator, modelBuilder, logger)
{
}
public CustomQueryProvider(DbConnection connection,
ISqlExpressionVisitor sqlGenerator,
IModelBuilder modelBuilder,
ILogger logger)
: base(connection, sqlGenerator, modelBuilder, logger)
{
}
public override DbProviderFactory DbProviderFactory => CustomDbProviderFactory.Instance;
public override IBulkInsert CreateBulkInsert()
{
if (IsExternalConnection)
{
return new CustomBulkInsert(Connection as CustomConnection);
}
else
{
return new CustomBulkInsert(ConnectionString);
}
}
public override IBulkUpdate CreateBulkUpdate()
{
if (IsExternalConnection)
{
return new CustomBulkUpdate(Connection as CustomConnection);
}
else
{
return new CustomBulkUpdate(ConnectionString);
}
}
protected override IDatabaseSchemaLoader GetSchemaLoader()
{
throw new NotImplementedException();
}
}
public class CustomQuerySqlGenerator : DefaultQuerySqlGenerator
{
public CustomQuerySqlGenerator(IDatabaseMapper databaseMapper)
: base(databaseMapper)
{ }
}
public class CustomQueryProviderFactory : IQueryProviderFactory
{
public Query.IQueryProvider Create(DbConnection connection, IModelBuilder modelBuilder, IDatabaseMapper databaseMapper) =>
new CustomQueryProvider(connection, new CustomQuerySqlGenerator(databaseMapper), modelBuilder, new Logger());
public Query.IQueryProvider Create(ConnectionStringSettings connectionString, IModelBuilder modelBuilder, IDatabaseMapper databaseMapper) =>
new CustomQueryProvider(connectionString, new CustomQuerySqlGenerator(databaseMapper), modelBuilder, new Logger());
public static void Register()
{
QueryProviderFactories.Register<CustomConnection>("System.Data.CustomDb", new CustomQueryProviderFactory());
}
}
For simple integration into ASP.NET Core projects, the Kros.KORM.Extensions.Asp package was created.
You can use the AddKorm
extension method to register IDatabase
to the DI container.
public void ConfigureServices(IServiceCollection services)
{
services.AddKorm(Configuration);
}
The configuration file (typically appsettings.json
) must contain a section ConnectionString
.
"ConnectionString": {
"ProviderName": "System.Data.SqlClient",
"ConnectionString": "Server=servername\\instancename;Initial Catalog=database;Persist Security Info=False;"
}
If you need to initialize the database for IIdGenerator then you can call InitDatabaseForIdGenerator
.
public void ConfigureServices(IServiceCollection services)
{
services.AddKorm(Configuration)
.InitDatabaseForIdGenerator();
}
Kros.KORM unit test coverage is more than 87%. There are also some performance test written for Kros.KORM. Here you can see some of their results:
- Reading of 150 000 records with 25 columns (long strings and guids) from DataTable is finished in about 410 ms.
- Reading of 1 500 records with 25 columns (long strings and guids) from DataTable is finished in about 7 ms.