Skip to content

Commit

Permalink
Support data annotations for POCO mapping #17 + refactoring code that…
Browse files Browse the repository at this point in the history
… performs mapping + fix issue with SelectQuery.Single/ToList for types without default constructor (like string)
  • Loading branch information
VitaliyMF committed Sep 17, 2016
1 parent a5ce496 commit fd4e440
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 99 deletions.
44 changes: 44 additions & 0 deletions src/NReco.Data.Tests/DbDataAdapterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

using Xunit;

Expand Down Expand Up @@ -58,6 +60,12 @@ public void Select() {
Assert.Equal(2, contactsWithHightRS.Count );
Assert.Equal(5, contactsWithHightRS.Columns.Count );
Assert.Equal("Viola Garrett", contactsWithHightRS[0]["name"] );

// select to annotated object
var companies = DbAdapter.Select(new Query("companies").OrderBy("id")).ToList<CompanyModelAnnotated>();
Assert.Equal(2, companies.Count);
Assert.Equal("Microsoft", companies[0].Name);

}

[Fact]
Expand Down Expand Up @@ -178,13 +186,49 @@ public async Task InsertUpdateDeleteAsync_RecordSet() {

Assert.Equal(2, await DbAdapter.DeleteAsync(new Query("companies", (QField)"id">= new QConst(newCompany1Row["id"]) )) );
}

[Fact]
public void InsertUpdateDelete_PocoModel() {
// insert
var newCompany = new CompanyModelAnnotated();
newCompany.Id = 5000; // should be ignored
newCompany.Name = "Test Super Corp";
newCompany.registered = false; // should be ignored
DbAdapter.Insert("companies", newCompany);

Assert.True(newCompany.Id.HasValue);
Assert.NotEqual(5000, newCompany.Id.Value);

Assert.Equal("Test Super Corp", DbAdapter.Select(new Query("companies", (QField)"id"==(QConst)newCompany.Id.Value).Select("title") ).Single<string>() );

newCompany.Name = "Super Corp updated";
Assert.Equal(1, DbAdapter.Update(new Query("companies", (QField)"id"==(QConst)newCompany.Id.Value ), newCompany) );

Assert.Equal(newCompany.Name, DbAdapter.Select(new Query("companies", (QField)"id"==(QConst)newCompany.Id.Value).Select("title") ).Single<string>() );

Assert.Equal(1, DbAdapter.Delete( new Query("companies", (QField)"id"==(QConst)newCompany.Id.Value )) );
}


public class ContactModel {
public int? id { get; set; }
public string name { get; set; }
public int? company_id { get; set; }
}

public class CompanyModelAnnotated {

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Column("id")]
public int? Id { get; set; }

[Column("title")]
public string Name { get; set; }

[NotMapped]
public bool registered { get; set; }
}

}
}
1 change: 1 addition & 0 deletions src/NReco.Data.Tests/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"System.Data.SqlClient": "4.1.0",
"Microsoft.Data.SQLite": "1.0.0",
"System.ComponentModel.Annotations": "4.1.0",
"NReco.Data": {
"target": "project"
}
Expand Down
44 changes: 8 additions & 36 deletions src/NReco.Data/DataHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System.Threading.Tasks;
using System.Data;
using System.Data.Common;
using System.Reflection;
using System.IO;

namespace NReco.Data {
Expand Down Expand Up @@ -154,35 +155,6 @@ internal static RecordSet GetRecordSetByReader(IDataReader rdr) {
return rs;
}

internal static void MapTo(IDataRecord record, object o, Func<string,string> getPropertyName) {
var type = o.GetType();
for (int i = 0; i < record.FieldCount; i++) {
var fieldName = record.GetName(i);
var fieldValue = record.GetValue(i);

var propName = (getPropertyName!=null ? getPropertyName(fieldName) : null) ?? fieldName;
var pInfo =type.GetProperty(propName);
if (pInfo!=null) {
if (IsNullOrDBNull(fieldValue)) {
fieldValue = null;
if (Nullable.GetUnderlyingType(pInfo.PropertyType) == null && pInfo.PropertyType._IsValueType() )
fieldValue = Activator.CreateInstance(pInfo.PropertyType);
} else {
var propType = pInfo.PropertyType;
if (Nullable.GetUnderlyingType(propType) != null)
propType = Nullable.GetUnderlyingType(propType);

if (propType._IsEnum()) {
fieldValue = Enum.Parse(propType, fieldValue.ToString(), true);
} else {
fieldValue = Convert.ChangeType(fieldValue, propType, System.Globalization.CultureInfo.InvariantCulture);
}
}
pInfo.SetValue(o, fieldValue, null);
}
}
}

internal static IEnumerable<KeyValuePair<string, IQueryValue>> GetChangeset(IDictionary data) {
if (data == null)
yield break;
Expand All @@ -201,17 +173,17 @@ internal static IEnumerable<KeyValuePair<string, IQueryValue>> GetChangeset(IDic
}
}

internal static IEnumerable<KeyValuePair<string, IQueryValue>> GetChangeset(object o, IDictionary<string,string> propertyToFieldMap) {
internal static IEnumerable<KeyValuePair<string, IQueryValue>> GetChangeset(object o, DataMapper dtoMapper) {
if (o == null)
yield break;
var oType = o.GetType();
foreach (var p in oType.GetProperties()) {
var pVal = p.GetValue(o, null);
var schema = (dtoMapper??DataMapper.Instance).GetSchema(oType);
foreach (var columnMapping in schema.Columns) {
if (columnMapping.IsReadOnly || columnMapping.GetVal==null)
continue;
var pVal = columnMapping.GetVal(o);
var qVal = pVal is IQueryValue ? (IQueryValue)pVal : new QConst(pVal);
var fldName = p.Name;
if (propertyToFieldMap!=null)
if (!propertyToFieldMap.TryGetValue(fldName, out fldName))
continue;
var fldName = columnMapping.ColumnName;
yield return new KeyValuePair<string, IQueryValue>(fldName, qVal );
}
}
Expand Down
220 changes: 220 additions & 0 deletions src/NReco.Data/DataMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#region License
/*
* NReco Data library (http://www.nrecosite.com/)
* Copyright 2016 Vitaliy Fedorchenko
* Distributed under the MIT license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#endregion


using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using System.Data;
using System.Data.Common;
using System.Reflection;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.IO;

namespace NReco.Data {

internal class DataMapper {

internal readonly static DataMapper Instance = new DataMapper();

IDictionary<Type,PocoModelSchema> SchemaCache;

internal DataMapper() {
SchemaCache = new ConcurrentDictionary<Type, PocoModelSchema>();
}

PocoModelSchema InferSchema(Type t) {
var keyCols = new List<ColumnMapping>();
var cols = new List<ColumnMapping>();
foreach (var prop in t.GetProperties()) {
var metadata = CheckSchemaAttributes(prop.GetCustomAttributes());
if (metadata.Item3) // not mapped
continue;
var colMapping = new ColumnMapping(
metadata.Item1 ?? prop.Name, t, prop.Name, prop.PropertyType,
prop.CanRead, prop.CanWrite,
metadata.Item4, metadata.Item5);
if (metadata.Item2) // is key
keyCols.Add(colMapping);
cols.Add(colMapping);
}
foreach (var fld in t.GetFields()) {
var metadata = CheckSchemaAttributes(fld.GetCustomAttributes());
if (metadata.Item3) // not mapped
continue;
var colMapping = new ColumnMapping(
metadata.Item1 ?? fld.Name, t, fld.Name, fld.FieldType,
true, true,
metadata.Item4, metadata.Item5);
if (metadata.Item2) // is key
keyCols.Add(colMapping);
cols.Add(colMapping);
}
return new PocoModelSchema(cols.ToArray(), keyCols.ToArray() );
}

internal PocoModelSchema GetSchema(Type t) {
PocoModelSchema schema = null;
if (!SchemaCache.TryGetValue(t, out schema)) {
schema = InferSchema(t);
SchemaCache[t] = schema;
}
return schema;
}

Tuple<string,bool,bool,bool,bool> CheckSchemaAttributes(IEnumerable<Attribute> attrs) {
bool isNotMapped = false;
bool isKey = false;
bool isDbGenerated = false;
bool isIdentity = false;
string colName = null;
foreach (var attr in attrs) {
if (attr is NotMappedAttribute) {
isNotMapped = true;
break;
} else if (attr is KeyAttribute) {
isKey = true;
} else if (attr is ColumnAttribute) {
var colAttr = (ColumnAttribute)attr;
colName = colAttr.Name;
} else if (attr is DatabaseGeneratedAttribute) {
var dbGenAttr = ((DatabaseGeneratedAttribute)attr);
isDbGenerated = true;
if (dbGenAttr.DatabaseGeneratedOption==DatabaseGeneratedOption.Identity)
isIdentity = true;
}
}
return new Tuple<string,bool,bool,bool,bool>(colName, isKey, isNotMapped, isDbGenerated, isIdentity);
}

internal void MapTo(IDataRecord record, object o) {
if (o==null)
return;
var type = o.GetType();
var schema = GetSchema(type);

for (int i = 0; i < record.FieldCount; i++) {
var fieldName = record.GetName(i);
var colMapping = schema.GetColumnMapping(fieldName);
if (colMapping==null || colMapping.SetVal==null)
continue;

var fieldValue = record.GetValue(i);

if (DataHelper.IsNullOrDBNull(fieldValue)) {
fieldValue = null;
if (Nullable.GetUnderlyingType(colMapping.ValueType) == null && colMapping.ValueType._IsValueType() )
fieldValue = Activator.CreateInstance(colMapping.ValueType); // slow: TBD faster way to get default(T)
}
colMapping.SetValue(o, fieldValue);
}
}

internal class PocoModelSchema {

internal readonly ColumnMapping[] Key;

internal readonly ColumnMapping[] Columns;

Dictionary<string,ColumnMapping> ColNameMap;

internal PocoModelSchema(ColumnMapping[] cols, ColumnMapping[] key) {
Columns = cols;
Key = key;
ColNameMap = new Dictionary<string, ColumnMapping>(Columns.Length);
for (int i=0; i<Columns.Length; i++) {
ColNameMap[Columns[i].ColumnName] = Columns[i];
}
}

internal ColumnMapping GetColumnMapping(string colName) {
ColumnMapping colMapping = null;
ColNameMap.TryGetValue(colName, out colMapping);
return colMapping;
}
}

internal class ColumnMapping {
internal readonly string ColumnName;
internal readonly Type ValueType;

internal readonly Func<object,object> GetVal;
internal readonly Action<object,object> SetVal;

internal readonly bool IsReadOnly;

internal readonly bool IsIdentity;

internal ColumnMapping(
string colName, Type t,
string propOrFieldName, Type propOrFieldType,
bool canRead, bool canWrite,
bool isReadOnly, bool isIdentity) {
ColumnName = colName;
ValueType = propOrFieldType;
IsReadOnly = isReadOnly;
IsIdentity = isIdentity;

// compose get
if (canRead) {
var getParamObj = Expression.Parameter(typeof(object));
var getterExpr = Expression.Lambda<Func<object,object>>(
Expression.Convert(
Expression.PropertyOrField( Expression.Convert(getParamObj,t), propOrFieldName ),
typeof(object)
),
getParamObj
);
GetVal = getterExpr.Compile();
}

// compose set
if (canWrite) {
var setParamObj = Expression.Parameter(typeof(object));
var setParamVal = Expression.Parameter(typeof(object));
var setterExpr = Expression.Lambda<Action<object,object>>(
Expression.Assign(
Expression.PropertyOrField( Expression.Convert(setParamObj,t) , propOrFieldName ),
Expression.Convert(setParamVal, propOrFieldType)
), setParamObj, setParamVal
);
SetVal = setterExpr.Compile();
}
}

internal bool SetValue(object obj, object val) {
if (SetVal==null)
return false;
var valType = ValueType;
if (Nullable.GetUnderlyingType(valType) != null)
valType = Nullable.GetUnderlyingType(valType);
if (valType._IsEnum()) {
val = Enum.Parse(valType, val.ToString(), true);
}

SetVal(obj, Convert.ChangeType(val, valType ) );
return true;
}

}


}
}
6 changes: 0 additions & 6 deletions src/NReco.Data/DbCommandBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,13 @@ public static IDbCommand GetUpdateCommand(this IDbCommandBuilder cmdBuilder, Que
public static IDbCommand GetUpdateCommand(this IDbCommandBuilder cmdBuilder, Query q, object poco) {
return cmdBuilder.GetUpdateCommand(q, DataHelper.GetChangeset(poco, null) );
}
public static IDbCommand GetUpdateCommand(this IDbCommandBuilder cmdBuilder, Query q, object poco, IDictionary<string,string> propertyToFieldMap) {
return cmdBuilder.GetUpdateCommand(q, DataHelper.GetChangeset(poco, propertyToFieldMap) );
}

public static IDbCommand GetInsertCommand(this IDbCommandBuilder cmdBuilder, string table, IDictionary<string,object> data) {
return cmdBuilder.GetInsertCommand(table, DataHelper.GetChangeset(data) );
}
public static IDbCommand GetInsertCommand(this IDbCommandBuilder cmdBuilder, string table, object poco) {
return cmdBuilder.GetInsertCommand(table, DataHelper.GetChangeset(poco, null) );
}
public static IDbCommand GetInsertCommand(this IDbCommandBuilder cmdBuilder, string table, object poco, IDictionary<string,string> propertyToFieldMap) {
return cmdBuilder.GetInsertCommand(table, DataHelper.GetChangeset(poco, propertyToFieldMap) );
}

}
}
Loading

0 comments on commit fd4e440

Please sign in to comment.