Commit a67ed727 authored by johandanforth's avatar johandanforth

Cleaned up shared files in Contrib and tests. Also added tests for transactions in Contrib

parent c8bd264d
......@@ -44,7 +44,10 @@
<Compile Include="..\Dapper.Contrib\Properties\AssemblyInfo.cs">
<Link>AssemblyInfo.cs</Link>
</Compile>
<Compile Include="SqlMapperExtensions.cs" />
<Compile Include="..\Dapper.Contrib\SqlMapperExtensions.cs">
<Link>SqlMapperExtensions.cs</Link>
</Compile>
<Compile Include="SqlMapperExtensionsAsync.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
......
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Dapper;
#pragma warning disable 1573, 1591 // xml comments
namespace Dapper.Contrib.Extensions
{
public static partial class SqlMapperExtensions
{
/// <summary>
/// Returns a single entity by a single id from table "Ts" asynchronously using .NET 4.5 Task. T must be of interface type.
/// Id must be marked with [Key] attribute.
/// Created entity is tracked/intercepted for changes and used by the Update() extension.
/// </summary>
/// <typeparam name="T">Interface type to create and populate</typeparam>
/// <param name="connection">Open SqlConnection</param>
/// <param name="id">Id of the entity to get, must be marked with [Key] attribute</param>
/// <returns>Entity of T</returns>
public static async Task<T> GetAsync<T>(this IDbConnection connection, dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
var type = typeof(T);
string sql;
if (!GetQueries.TryGetValue(type.TypeHandle, out sql))
{
var keys = KeyPropertiesCache(type);
if (keys.Count() > 1)
throw new DataException("Get<T> only supports an entity with a single [Key] property");
if (!keys.Any())
throw new DataException("Get<T> only supports en entity with a [Key] property");
var onlyKey = keys.First();
var name = GetTableName(type);
// TODO: pluralizer
// TODO: query information schema and only select fields that are both in information schema and underlying class / interface
sql = "select * from " + name + " where " + onlyKey.Name + " = @id";
GetQueries[type.TypeHandle] = sql;
}
var dynParms = new DynamicParameters();
dynParms.Add("@id", id);
T obj;
if (type.IsInterface)
{
var res = (await connection.QueryAsync<dynamic>(sql, dynParms).ConfigureAwait(false)).FirstOrDefault() as IDictionary<string, object>;
if (res == null)
return null;
obj = ProxyGenerator.GetInterfaceProxy<T>();
foreach (var property in TypePropertiesCache(type))
{
var val = res[property.Name];
property.SetValue(obj, val, null);
}
((IProxy)obj).IsDirty = false; //reset change tracking and return
}
else
{
obj = (await connection.QueryAsync<T>(sql, dynParms, transaction, commandTimeout).ConfigureAwait(false)).FirstOrDefault();
}
return obj;
}
/// <summary>
/// Inserts an entity into table "Ts" asynchronously using .NET 4.5 Task and returns identity id.
/// </summary>
/// <param name="connection">Open SqlConnection</param>
/// <param name="entityToInsert">Entity to insert</param>
/// <returns>Identity of inserted entity</returns>
public static Task<int> InsertAsync<T>(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
var type = typeof(T);
var name = GetTableName(type);
var sbColumnList = new StringBuilder(null);
var allProperties = TypePropertiesCache(type);
var keyProperties = KeyPropertiesCache(type);
var computedProperties = ComputedPropertiesCache(type);
var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList();
for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count(); i++)
{
var property = allPropertiesExceptKeyAndComputed.ElementAt(i);
sbColumnList.AppendFormat("[{0}]", property.Name);
if (i < allPropertiesExceptKeyAndComputed.Count() - 1)
sbColumnList.Append(", ");
}
var sbParameterList = new StringBuilder(null);
for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count(); i++)
{
var property = allPropertiesExceptKeyAndComputed.ElementAt(i);
sbParameterList.AppendFormat("@{0}", property.Name);
if (i < allPropertiesExceptKeyAndComputed.Count() - 1)
sbParameterList.Append(", ");
}
var adapter = GetFormatter(connection);
return adapter.InsertAsync(connection, transaction, commandTimeout, name, sbColumnList.ToString(),
sbParameterList.ToString(), keyProperties, entityToInsert);
}
/// <summary>
/// Updates entity in table "Ts" asynchronously using .NET 4.5 Task, checks if the entity is modified if the entity is tracked by the Get() extension.
/// </summary>
/// <typeparam name="T">Type to be updated</typeparam>
/// <param name="connection">Open SqlConnection</param>
/// <param name="entityToUpdate">Entity to be updated</param>
/// <returns>true if updated, false if not found or not modified (tracked entities)</returns>
public static async Task<bool> UpdateAsync<T>(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
var proxy = entityToUpdate as IProxy;
if (proxy != null)
{
if (!proxy.IsDirty) return false;
}
var type = typeof(T);
var keyProperties = KeyPropertiesCache(type);
if (!keyProperties.Any())
throw new ArgumentException("Entity must have at least one [Key] property");
var name = GetTableName(type);
var sb = new StringBuilder();
sb.AppendFormat("update {0} set ", name);
var allProperties = TypePropertiesCache(type);
var computedProperties = ComputedPropertiesCache(type);
var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList();
for (var i = 0; i < nonIdProps.Count(); i++)
{
var property = nonIdProps.ElementAt(i);
sb.AppendFormat("{0} = @{1}", property.Name, property.Name);
if (i < nonIdProps.Count() - 1)
sb.AppendFormat(", ");
}
sb.Append(" where ");
for (var i = 0; i < keyProperties.Count(); i++)
{
var property = keyProperties.ElementAt(i);
sb.AppendFormat("{0} = @{1}", property.Name, property.Name);
if (i < keyProperties.Count() - 1)
sb.AppendFormat(" and ");
}
var updated = await connection.ExecuteAsync(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction).ConfigureAwait(false);
return updated > 0;
}
/// <summary>
/// Delete entity in table "Ts" asynchronously using .NET 4.5 Task.
/// </summary>
/// <typeparam name="T">Type of entity</typeparam>
/// <param name="connection">Open SqlConnection</param>
/// <param name="entityToDelete">Entity to delete</param>
/// <returns>true if deleted, false if not found</returns>
public static async Task<bool> DeleteAsync<T>(this IDbConnection connection, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
if (entityToDelete == null)
throw new ArgumentException("Cannot Delete null Object", "entityToDelete");
var type = typeof(T);
var keyProperties = KeyPropertiesCache(type);
if (!keyProperties.Any())
throw new ArgumentException("Entity must have at least one [Key] property");
var name = GetTableName(type);
var sb = new StringBuilder();
sb.AppendFormat("delete from {0} where ", name);
for (var i = 0; i < keyProperties.Count(); i++)
{
var property = keyProperties.ElementAt(i);
sb.AppendFormat("{0} = @{1}", property.Name, property.Name);
if (i < keyProperties.Count() - 1)
sb.AppendFormat(" and ");
}
var deleted = await connection.ExecuteAsync(sb.ToString(), entityToDelete, transaction, commandTimeout).ConfigureAwait(false);
return deleted > 0;
}
/// <summary>
/// Delete all entities in the table related to the type T asynchronously using .NET 4.5 Task.
/// </summary>
/// <typeparam name="T">Type of entity</typeparam>
/// <param name="connection">Open SqlConnection</param>
/// <returns>true if deleted, false if none found</returns>
public static async Task<bool> DeleteAllAsync<T>(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
var type = typeof(T);
var name = GetTableName(type);
var statement = String.Format("delete from {0}", name);
var deleted = await connection.ExecuteAsync(statement, null, transaction, commandTimeout).ConfigureAwait(false);
return deleted > 0;
}
}
}
public partial interface ISqlAdapter
{
Task<int> InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert);
}
public partial class SqlServerAdapter
{
public async Task<int> InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert)
{
var cmd = String.Format("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList);
await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false);
//NOTE: would prefer to use IDENT_CURRENT('tablename') or IDENT_SCOPE but these are not available on SQLCE
var r = await connection.QueryAsync<dynamic>("select @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false);
var id = (int)r.First().id;
var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray();
if (propertyInfos.Any())
propertyInfos.First().SetValue(entityToInsert, id, null);
return id;
}
}
public partial class PostgresAdapter
{
public async Task<int> InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert)
{
var sb = new StringBuilder();
sb.AppendFormat("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList);
// If no primary key then safe to assume a join table with not too much data to return
var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray();
if (!propertyInfos.Any())
sb.Append(" RETURNING *");
else
{
sb.Append(" RETURNING ");
bool first = true;
foreach (var property in propertyInfos)
{
if (!first)
sb.Append(", ");
first = false;
sb.Append(property.Name);
}
}
var results = await connection.QueryAsync<dynamic>(sb.ToString(), entityToInsert, transaction, commandTimeout).ConfigureAwait(false);
// Return the key by assinging the corresponding property in the object - by product is that it supports compound primary keys
var id = 0;
foreach (var p in propertyInfos)
{
var value = ((IDictionary<string, object>)results.First())[p.Name.ToLower()];
p.SetValue(entityToInsert, value, null);
if (id == 0)
id = Convert.ToInt32(value);
}
return id;
}
}
public partial class SQLiteAdapter
{
public async Task<int> InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert)
{
var cmd = String.Format("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList);
await connection.ExecuteAsync(cmd, entityToInsert, transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false);
var r = await connection.QueryAsync<dynamic>("select last_insert_rowid() id", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false);
var id = (int)r.First().id;
var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray();
if (propertyInfos.Any())
propertyInfos.First().SetValue(entityToInsert, id, null);
return id;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace Dapper.Contrib.Tests_NET45
{
/// <summary>
/// Assert extensions borrowed from Sam's code in DapperTests
/// </summary>
static class Assert
{
public static void IsEqualTo<T>(this T obj, T other)
{
if (!obj.Equals(other))
{
throw new ApplicationException(string.Format("{0} should be equals to {1}", obj, other));
}
}
public static void IsSequenceEqualTo<T>(this IEnumerable<T> obj, IEnumerable<T> other)
{
if (!obj.SequenceEqual(other))
{
throw new ApplicationException(string.Format("{0} should be equals to {1}", obj, other));
}
}
public static void IsFalse(this bool b)
{
if (b)
{
throw new ApplicationException("Expected false");
}
}
public static void IsTrue(this bool b)
{
if (!b)
{
throw new ApplicationException("Expected true");
}
}
public static void IsNull(this object obj)
{
if (obj != null)
{
throw new ApplicationException("Expected null");
}
}
}
}
\ No newline at end of file
......@@ -7,8 +7,8 @@
<ProjectGuid>{7A85178F-4ADC-4E4C-BF08-17FC99488A9A}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Dapper.Contrib.Tests_NET45</RootNamespace>
<AssemblyName>Dapper.Contrib.Tests NET45</AssemblyName>
<RootNamespace>Dapper.Contrib.Tests</RootNamespace>
<AssemblyName>Dapper.Contrib.Tests</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
......@@ -35,6 +35,7 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
......@@ -42,10 +43,14 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Assert.cs" />
<Compile Include="..\Dapper.Contrib.Tests\Assert.cs">
<Link>Assert.cs</Link>
</Compile>
<Compile Include="..\Dapper.Contrib.Tests\Tests.cs">
<Link>Tests.cs</Link>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Tests.cs" />
<Compile Include="TestsAsync.cs" />
</ItemGroup>
<ItemGroup>
......
using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Dapper.Contrib.Tests_NET45
namespace Dapper.Contrib.Tests
{
class Program
{
......@@ -46,51 +42,22 @@ private static void RunTests()
{
var tester = new Tests();
foreach (var method in typeof(Tests).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
if (method.ReturnType != typeof(Task))
{
Console.Write("Running " + method.Name);
method.Invoke(tester, null);
Console.WriteLine(" - OK!");
}
}
}
private static void RunAsyncTests()
{
var tester = new TestsAsync();
Console.Write("Running TableNameAsync");
Task.WaitAll(tester.TableNameAsync());
Console.WriteLine(" - OK!");
Console.Write("Running TestSimpleGetAsync");
Task.WaitAll(tester.TestSimpleGetAsync());
Console.WriteLine(" - OK!");
Console.Write("Running InsertGetUpdateAsync");
Task.WaitAll(tester.InsertGetUpdateAsync());
Console.WriteLine(" - OK!");
Console.Write("Running InsertCheckKeyAsync");
Task.WaitAll(tester.InsertCheckKeyAsync());
Console.WriteLine(" - OK!");
Console.Write("Running BuilderSelectClauseAsync");
Task.WaitAll(tester.BuilderSelectClauseAsync());
Console.WriteLine(" - OK!");
Console.Write("Running BuilderTemplateWOCompositionAsync");
Task.WaitAll(tester.BuilderTemplateWOCompositionAsync());
Console.WriteLine(" - OK!");
Console.Write("Running InsertFieldWithReservedNameAsync");
Task.WaitAll(tester.InsertFieldWithReservedNameAsync());
Console.WriteLine(" - OK!");
Console.Write("Running DeleteAllAsync");
Task.WaitAll(tester.DeleteAllAsync());
foreach (var method in typeof(TestsAsync).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
Console.Write("Running " + method.Name);
Task.WaitAll((Task)method.Invoke(tester, null));
Console.WriteLine(" - OK!");
}
}
}
}
using System.Data;
using System.Data.SqlServerCe;
using System.IO;
using System.Linq;
using System.Reflection;
using Dapper.Contrib.Extensions;
using System.Collections.Generic;
using System;
using Dapper;
namespace Dapper.Contrib.Tests_NET45
{
public interface IUser
{
[Key]
int Id { get; set; }
string Name { get; set; }
int Age { get; set; }
}
public class User : IUser
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
[Table("Automobiles")]
public class Car
{
public int Id { get; set; }
public string Name { get; set; }
}
[Table("Results")]
public class Result
{
public int Id { get; set; }
public string Name { get; set; }
public int Order { get; set; }
}
public class Tests
{
private IDbConnection GetOpenConnection()
{
var projLoc = Assembly.GetAssembly(GetType()).Location;
var projFolder = Path.GetDirectoryName(projLoc);
var connection = new SqlCeConnection("Data Source = " + projFolder + "\\Test.sdf;");
connection.Open();
return connection;
}
public void TableName()
{
using (var connection = GetOpenConnection())
{
// tests against "Automobiles" table (Table attribute)
connection.Insert(new Car {Name = "Volvo"});
connection.Get<Car>(1).Name.IsEqualTo("Volvo");
connection.Update(new Car() {Id = 1, Name = "Saab"}).IsEqualTo(true);
connection.Get<Car>(1).Name.IsEqualTo("Saab");
connection.Delete(new Car() {Id = 1}).IsEqualTo(true);
connection.Get<Car>(1).IsNull();
}
}
public void TestSimpleGet()
{
using (var connection = GetOpenConnection())
{
var id = connection.Insert(new User { Name = "Adama", Age = 10 });
var user = connection.Get<User>(id);
user.Id.IsEqualTo((int)id);
user.Name.IsEqualTo("Adama");
connection.Delete(user);
}
}
public void InsertGetUpdate()
{
using (var connection = GetOpenConnection())
{
connection.Get<User>(3).IsNull();
var id = connection.Insert(new User {Name = "Adam", Age = 10});
//get a user with "isdirty" tracking
var user = connection.Get<IUser>(id);
user.Name.IsEqualTo("Adam");
connection.Update(user).IsEqualTo(false); //returns false if not updated, based on tracking
user.Name = "Bob";
connection.Update(user).IsEqualTo(true); //returns true if updated, based on tracking
user = connection.Get<IUser>(id);
user.Name.IsEqualTo("Bob");
//get a user with no tracking
var notrackedUser = connection.Get<User>(id);
notrackedUser.Name.IsEqualTo("Bob");
connection.Update(notrackedUser).IsEqualTo(true); //returns true, even though user was not changed
notrackedUser.Name = "Cecil";
connection.Update(notrackedUser).IsEqualTo(true);
connection.Get<User>(id).Name.IsEqualTo("Cecil");
connection.Query<User>("select * from Users").Count().IsEqualTo(1);
connection.Delete(user).IsEqualTo(true);
connection.Query<User>("select * from Users").Count().IsEqualTo(0);
connection.Update(notrackedUser).IsEqualTo(false); //returns false, user not found
}
}
public void InsertCheckKey()
{
using (var connection = GetOpenConnection())
{
connection.Get<IUser>(3).IsNull();
User user = new User { Name = "Adamb", Age = 10 };
int id = (int)connection.Insert(user);
user.Id.IsEqualTo(id);
}
}
public void BuilderSelectClause()
{
using (var connection = GetOpenConnection())
{
var rand = new Random(8675309);
var data = new List<User>();
for (int i = 0; i < 100; i++)
{
var nU = new User { Age = rand.Next(70), Id = i, Name = Guid.NewGuid().ToString() };
data.Add(nU);
nU.Id = (int)connection.Insert<User>(nU);
}
var builder = new SqlBuilder();
var justId = builder.AddTemplate("SELECT /**select**/ FROM Users");
var all = builder.AddTemplate("SELECT Name, /**select**/, Age FROM Users");
builder.Select("Id");
var ids = connection.Query<int>(justId.RawSql, justId.Parameters);
var users = connection.Query<User>(all.RawSql, all.Parameters);
foreach (var u in data)
{
if (!ids.Any(i => u.Id == i)) throw new Exception("Missing ids in select");
if (!users.Any(a => a.Id == u.Id && a.Name == u.Name && a.Age == u.Age)) throw new Exception("Missing users in select");
}
}
}
public void BuilderTemplateWOComposition()
{
var builder = new SqlBuilder();
var template = builder.AddTemplate("SELECT COUNT(*) FROM Users WHERE Age = @age", new {age = 5});
if (template.RawSql == null) throw new Exception("RawSql null");
if (template.Parameters == null) throw new Exception("Parameters null");
using (var connection = GetOpenConnection())
{
connection.Insert(new User { Age = 5, Name = "Testy McTestington" });
if (connection.Query<int>(template.RawSql, template.Parameters).Single() != 1)
throw new Exception("Query failed");
}
}
public void InsertFieldWithReservedName()
{
using (var conneciton = GetOpenConnection())
{
var id = conneciton.Insert(new Result() { Name = "Adam", Order = 1 });
var result = conneciton.Get<Result>(id);
result.Order.IsEqualTo(1);
}
}
public void DeleteAll()
{
using (var connection = GetOpenConnection())
{
var id1 = connection.Insert(new User() { Name = "Alice", Age = 32 });
var id2 = connection.Insert(new User() { Name = "Bob", Age = 33 });
connection.DeleteAll<User>();
connection.Get<User>(id1).IsNull();
connection.Get<User>(id2).IsNull();
}
}
}
}
......@@ -10,7 +10,7 @@
using System.Threading.Tasks;
namespace Dapper.Contrib.Tests_NET45
namespace Dapper.Contrib.Tests
{
public class TestsAsync
{
......
......@@ -44,10 +44,8 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>Dependencies\System.Data.SqlServerCe.dll</HintPath>
</Reference>
<Reference Include="System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91, processorArchitecture=MSIL" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
......
using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace Dapper.Contrib.Tests
{
......@@ -15,6 +11,8 @@ static void Main(string[] args)
{
Setup();
RunTests();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
private static void Setup()
......@@ -46,9 +44,6 @@ private static void RunTests()
method.Invoke(tester, null);
Console.WriteLine(" - OK!");
}
Console.WriteLine("(end of tests; press any key)");
Console.ReadKey();
}
}
......
......@@ -3,10 +3,10 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Transactions;
using Dapper.Contrib.Extensions;
using System.Collections.Generic;
using System;
using Dapper;
namespace Dapper.Contrib.Tests
......@@ -31,6 +31,8 @@ public class Car
{
public int Id { get; set; }
public string Name { get; set; }
[Computed]
public string Computed { get; set; }
}
[Table("Results")]
......@@ -53,16 +55,25 @@ private IDbConnection GetOpenConnection()
return connection;
}
private IDbConnection GetConnection()
{
var projLoc = Assembly.GetAssembly(GetType()).Location;
var projFolder = Path.GetDirectoryName(projLoc);
var connection = new SqlCeConnection("Data Source = " + projFolder + "\\Test.sdf;");
return connection;
}
public void TableName()
{
using (var connection = GetOpenConnection())
{
// tests against "Automobiles" table (Table attribute)
connection.Insert(new Car {Name = "Volvo"});
connection.Insert(new Car { Name = "Volvo" }).IsEqualTo(1);
connection.Get<Car>(1).Name.IsEqualTo("Volvo");
connection.Update(new Car() {Id = 1, Name = "Saab"}).IsEqualTo(true);
connection.Update(new Car() { Id = 1, Name = "Saab" }).IsEqualTo(true);
connection.Get<Car>(1).Name.IsEqualTo("Saab");
connection.Delete(new Car() {Id = 1}).IsEqualTo(true);
connection.Delete(new Car() { Id = 1 }).IsEqualTo(true);
connection.Get<Car>(1).IsNull();
}
}
......@@ -85,7 +96,10 @@ public void InsertGetUpdate()
{
connection.Get<User>(3).IsNull();
var id = connection.Insert(new User {Name = "Adam", Age = 10});
//insert with computed attribute that should be ignored
connection.Insert(new Car { Name = "Volvo", Computed = "this property should be ignored" });
var id = connection.Insert(new User { Name = "Adam", Age = 10 });
//get a user with "isdirty" tracking
var user = connection.Get<IUser>(id);
......@@ -112,6 +126,41 @@ public void InsertGetUpdate()
}
}
public void Transactions()
{
using (var connection = GetOpenConnection())
{
var id = connection.Insert(new Car { Name = "one car" }); //insert outside transaction
var tran = connection.BeginTransaction();
var car = connection.Get<Car>(id, tran);
var orgName = car.Name;
car.Name = "Another car";
connection.Update(car, tran);
tran.Rollback();
car = connection.Get<Car>(id); //updates should have been rolled back
car.Name.IsEqualTo(orgName);
}
}
public void TransactionScope()
{
using (var connection = GetConnection())
{
using (var txscope = new TransactionScope())
{
connection.Open(); //connection MUST be opened inside the transactionscope
var id = connection.Insert(new Car { Name = "one car" }); //inser car within transaction
txscope.Dispose(); //rollback
connection.Get<Car>(id).IsNull(); //returns null - car with that id should not exist
}
}
}
public void InsertCheckKey()
{
using (var connection = GetOpenConnection())
......@@ -156,7 +205,7 @@ public void BuilderSelectClause()
public void BuilderTemplateWOComposition()
{
var builder = new SqlBuilder();
var template = builder.AddTemplate("SELECT COUNT(*) FROM Users WHERE Age = @age", new {age = 5});
var template = builder.AddTemplate("SELECT COUNT(*) FROM Users WHERE Age = @age", new { age = 5 });
if (template.RawSql == null) throw new Exception("RawSql null");
if (template.Parameters == null) throw new Exception("Parameters null");
......@@ -188,7 +237,7 @@ public void DeleteAll()
{
var id1 = connection.Insert(new User() { Name = "Alice", Age = 32 });
var id2 = connection.Insert(new User() { Name = "Bob", Age = 33 });
connection.DeleteAll<User>();
connection.DeleteAll<User>().IsTrue();
connection.Get<User>(id1).IsNull();
connection.Get<User>(id2).IsNull();
}
......
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Collections.Concurrent;
using System.Reflection.Emit;
using System.Threading;
using System.Runtime.CompilerServices;
using Dapper;
#pragma warning disable 1573, 1591 // xml comments
......@@ -16,9 +15,10 @@
namespace Dapper.Contrib.Extensions
{
public static class SqlMapperExtensions
public static partial class SqlMapperExtensions
{
public interface IProxy
// ReSharper disable once MemberCanBePrivate.Global
public interface IProxy //must be kept public
{
bool IsDirty { get; set; }
}
......@@ -29,17 +29,18 @@ public interface IProxy
private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> GetQueries = new ConcurrentDictionary<RuntimeTypeHandle, string>();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> TypeTableName = new ConcurrentDictionary<RuntimeTypeHandle, string>();
private static readonly Dictionary<string, ISqlAdapter> AdapterDictionary = new Dictionary<string, ISqlAdapter>() {
private static readonly Dictionary<string, ISqlAdapter> AdapterDictionary = new Dictionary<string, ISqlAdapter> {
{"sqlconnection", new SqlServerAdapter()},
{"npgsqlconnection", new PostgresAdapter()},
{"sqliteconnection", new SQLiteAdapter()}
};
private static IEnumerable<PropertyInfo> ComputedPropertiesCache(Type type)
{
IEnumerable<PropertyInfo> pi;
if (ComputedProperties.TryGetValue(type.TypeHandle, out pi))
{
return pi;
return pi.ToList();
}
var computedProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ComputedAttribute)).ToList();
......@@ -47,13 +48,14 @@ private static IEnumerable<PropertyInfo> ComputedPropertiesCache(Type type)
ComputedProperties[type.TypeHandle] = computedProperties;
return computedProperties;
}
private static IEnumerable<PropertyInfo> KeyPropertiesCache(Type type)
private static List<PropertyInfo> KeyPropertiesCache(Type type)
{
IEnumerable<PropertyInfo> pi;
if (KeyProperties.TryGetValue(type.TypeHandle,out pi))
if (KeyProperties.TryGetValue(type.TypeHandle, out pi))
{
return pi;
return pi.ToList();
}
var allProperties = TypePropertiesCache(type);
......@@ -61,7 +63,7 @@ private static IEnumerable<PropertyInfo> KeyPropertiesCache(Type type)
if (keyProperties.Count == 0)
{
var idProp = allProperties.Where(p => p.Name.ToLower() == "id").FirstOrDefault();
var idProp = allProperties.FirstOrDefault(p => p.Name.ToLower() == "id");
if (idProp != null)
{
keyProperties.Add(idProp);
......@@ -71,36 +73,36 @@ private static IEnumerable<PropertyInfo> KeyPropertiesCache(Type type)
KeyProperties[type.TypeHandle] = keyProperties;
return keyProperties;
}
private static IEnumerable<PropertyInfo> TypePropertiesCache(Type type)
private static List<PropertyInfo> TypePropertiesCache(Type type)
{
IEnumerable<PropertyInfo> pis;
if (TypeProperties.TryGetValue(type.TypeHandle, out pis))
{
return pis;
return pis.ToList();
}
var properties = type.GetProperties().Where(IsWriteable).ToArray();
TypeProperties[type.TypeHandle] = properties;
return properties;
return properties.ToList();
}
public static bool IsWriteable(PropertyInfo pi)
private static bool IsWriteable(PropertyInfo pi)
{
object[] attributes = pi.GetCustomAttributes(typeof (WriteAttribute), false);
if (attributes.Length == 1)
{
WriteAttribute write = (WriteAttribute) attributes[0];
return write.Write;
}
return true;
var attributes = pi.GetCustomAttributes(typeof(WriteAttribute), false);
if (attributes.Length != 1) return true;
var writeAttribute = (WriteAttribute)attributes[0];
return writeAttribute.Write;
}
/// <summary>
/// Returns a single entity by a single id from table "Ts". T must be of interface type.
/// Returns a single entity by a single id from table "Ts".
/// Id must be marked with [Key] attribute.
/// Created entity is tracked/intercepted for changes and used by the Update() extension.
/// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension
/// for optimal performance.
/// </summary>
/// <typeparam name="T">Interface type to create and populate</typeparam>
/// <typeparam name="T">Interface or type to create and populate</typeparam>
/// <param name="connection">Open SqlConnection</param>
/// <param name="id">Id of the entity to get, must be marked with [Key] attribute</param>
/// <returns>Entity of T</returns>
......@@ -113,7 +115,7 @@ public static bool IsWriteable(PropertyInfo pi)
var keys = KeyPropertiesCache(type);
if (keys.Count() > 1)
throw new DataException("Get<T> only supports an entity with a single [Key] property");
if (keys.Count() == 0)
if (!keys.Any())
throw new DataException("Get<T> only supports en entity with a [Key] property");
var onlyKey = keys.First();
......@@ -129,14 +131,14 @@ public static bool IsWriteable(PropertyInfo pi)
var dynParms = new DynamicParameters();
dynParms.Add("@id", id);
T obj = null;
T obj;
if (type.IsInterface)
{
var res = connection.Query(sql, dynParms).FirstOrDefault() as IDictionary<string, object>;
if (res == null)
return (T)((object)null);
return null;
obj = ProxyGenerator.GetInterfaceProxy<T>();
......@@ -150,11 +152,10 @@ public static bool IsWriteable(PropertyInfo pi)
}
else
{
obj = connection.Query<T>(sql, dynParms, transaction: transaction, commandTimeout: commandTimeout).FirstOrDefault();
obj = connection.Query<T>(sql, dynParms, transaction, commandTimeout: commandTimeout).FirstOrDefault();
}
return obj;
}
private static string GetTableName(Type type)
{
string name;
......@@ -165,7 +166,7 @@ private static string GetTableName(Type type)
name = name.Substring(1);
//NOTE: This as dynamic trick should be able to handle both our own Table-attribute as well as the one in EntityFramework
var tableattr = type.GetCustomAttributes(false).Where(attr => attr.GetType().Name == "TableAttribute").SingleOrDefault() as
var tableattr = type.GetCustomAttributes(false).SingleOrDefault(attr => attr.GetType().Name == "TableAttribute") as
dynamic;
if (tableattr != null)
name = tableattr.Name;
......@@ -182,17 +183,13 @@ private static string GetTableName(Type type)
/// <returns>Identity of inserted entity</returns>
public static long Insert<T>(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
var type = typeof(T);
var name = GetTableName(type);
var sbColumnList = new StringBuilder(null);
var allProperties = TypePropertiesCache(type);
var keyProperties = KeyPropertiesCache(type);
var computedProperties = ComputedPropertiesCache(type);
var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties));
var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList();
for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count(); i++)
{
......@@ -210,9 +207,9 @@ private static string GetTableName(Type type)
if (i < allPropertiesExceptKeyAndComputed.Count() - 1)
sbParameterList.Append(", ");
}
ISqlAdapter adapter = GetFormatter(connection);
int id = adapter.Insert(connection, transaction, commandTimeout, name, sbColumnList.ToString(), sbParameterList.ToString(), keyProperties, entityToInsert);
return id;
var adapter = GetFormatter(connection);
return adapter.Insert(connection, transaction, commandTimeout, name, sbColumnList.ToString(),
sbParameterList.ToString(), keyProperties, entityToInsert);
}
/// <summary>
......@@ -243,7 +240,7 @@ private static string GetTableName(Type type)
var allProperties = TypePropertiesCache(type);
var computedProperties = ComputedPropertiesCache(type);
var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties));
var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList();
for (var i = 0; i < nonIdProps.Count(); i++)
{
......@@ -279,7 +276,7 @@ private static string GetTableName(Type type)
var type = typeof(T);
var keyProperties = KeyPropertiesCache(type);
if (keyProperties.Count() == 0)
if (!keyProperties.Any())
throw new ArgumentException("Entity must have at least one [Key] property");
var name = GetTableName(type);
......@@ -294,11 +291,10 @@ private static string GetTableName(Type type)
if (i < keyProperties.Count() - 1)
sb.AppendFormat(" and ");
}
var deleted = connection.Execute(sb.ToString(), entityToDelete, transaction: transaction, commandTimeout: commandTimeout);
var deleted = connection.Execute(sb.ToString(), entityToDelete, transaction, commandTimeout);
return deleted > 0;
}
/// <summary>
/// Delete all entities in the table related to the type T.
/// </summary>
......@@ -310,20 +306,19 @@ private static string GetTableName(Type type)
var type = typeof(T);
var name = GetTableName(type);
var statement = String.Format("delete from {0}", name);
var deleted = connection.Execute(statement, null, transaction: transaction, commandTimeout: commandTimeout);
var deleted = connection.Execute(statement, null, transaction, commandTimeout);
return deleted > 0;
}
public static ISqlAdapter GetFormatter(IDbConnection connection)
private static ISqlAdapter GetFormatter(IDbConnection connection)
{
string name = connection.GetType().Name.ToLower();
if (!AdapterDictionary.ContainsKey(name))
return new SqlServerAdapter();
return AdapterDictionary[name];
var name = connection.GetType().Name.ToLower();
return !AdapterDictionary.ContainsKey(name) ?
new SqlServerAdapter() :
AdapterDictionary[name];
}
class ProxyGenerator
static class ProxyGenerator
{
private static readonly Dictionary<Type, object> TypeCache = new Dictionary<Type, object>();
......@@ -335,14 +330,6 @@ private static AssemblyBuilder GetAsmBuilder(string name)
return assemblyBuilder;
}
public static T GetClassProxy<T>()
{
// A class proxy could be implemented if all properties are virtual
// otherwise there is a pretty dangerous case where internal actions will not update dirty tracking
throw new NotImplementedException();
}
public static T GetInterfaceProxy<T>()
{
Type typeOfT = typeof(T);
......@@ -356,7 +343,7 @@ public static T GetInterfaceProxy<T>()
var moduleBuilder = assemblyBuilder.DefineDynamicModule("SqlMapperExtensions." + typeOfT.Name); //NOTE: to save, add "asdasd.dll" parameter
var interfaceType = typeof(Dapper.Contrib.Extensions.SqlMapperExtensions.IProxy);
var interfaceType = typeof(IProxy);
var typeBuilder = moduleBuilder.DefineType(typeOfT.Name + "_" + Guid.NewGuid(),
TypeAttributes.Public | TypeAttributes.Class);
typeBuilder.AddInterfaceImplementation(typeOfT);
......@@ -390,7 +377,7 @@ private static MethodInfo CreateIsDirtyProperty(TypeBuilder typeBuilder)
var property = typeBuilder.DefineProperty("IsDirty",
System.Reflection.PropertyAttributes.None,
propType,
new Type[] { propType });
new[] { propType });
const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.SpecialName |
MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig;
......@@ -400,24 +387,24 @@ private static MethodInfo CreateIsDirtyProperty(TypeBuilder typeBuilder)
getSetAttr,
propType,
Type.EmptyTypes);
var currGetIL = currGetPropMthdBldr.GetILGenerator();
currGetIL.Emit(OpCodes.Ldarg_0);
currGetIL.Emit(OpCodes.Ldfld, field);
currGetIL.Emit(OpCodes.Ret);
var currGetIl = currGetPropMthdBldr.GetILGenerator();
currGetIl.Emit(OpCodes.Ldarg_0);
currGetIl.Emit(OpCodes.Ldfld, field);
currGetIl.Emit(OpCodes.Ret);
var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + "IsDirty",
getSetAttr,
null,
new Type[] { propType });
var currSetIL = currSetPropMthdBldr.GetILGenerator();
currSetIL.Emit(OpCodes.Ldarg_0);
currSetIL.Emit(OpCodes.Ldarg_1);
currSetIL.Emit(OpCodes.Stfld, field);
currSetIL.Emit(OpCodes.Ret);
new[] { propType });
var currSetIl = currSetPropMthdBldr.GetILGenerator();
currSetIl.Emit(OpCodes.Ldarg_0);
currSetIl.Emit(OpCodes.Ldarg_1);
currSetIl.Emit(OpCodes.Stfld, field);
currSetIl.Emit(OpCodes.Ret);
property.SetGetMethod(currGetPropMthdBldr);
property.SetSetMethod(currSetPropMthdBldr);
var getMethod = typeof(Dapper.Contrib.Extensions.SqlMapperExtensions.IProxy).GetMethod("get_" + "IsDirty");
var setMethod = typeof(Dapper.Contrib.Extensions.SqlMapperExtensions.IProxy).GetMethod("set_" + "IsDirty");
var getMethod = typeof(IProxy).GetMethod("get_" + "IsDirty");
var setMethod = typeof(IProxy).GetMethod("set_" + "IsDirty");
typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod);
typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod);
......@@ -431,7 +418,7 @@ private static void CreateProperty<T>(TypeBuilder typeBuilder, string propertyNa
var property = typeBuilder.DefineProperty(propertyName,
System.Reflection.PropertyAttributes.None,
propType,
new Type[] { propType });
new[] { propType });
const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.Virtual |
MethodAttributes.HideBySig;
......@@ -442,25 +429,25 @@ private static void CreateProperty<T>(TypeBuilder typeBuilder, string propertyNa
propType,
Type.EmptyTypes);
var currGetIL = currGetPropMthdBldr.GetILGenerator();
currGetIL.Emit(OpCodes.Ldarg_0);
currGetIL.Emit(OpCodes.Ldfld, field);
currGetIL.Emit(OpCodes.Ret);
var currGetIl = currGetPropMthdBldr.GetILGenerator();
currGetIl.Emit(OpCodes.Ldarg_0);
currGetIl.Emit(OpCodes.Ldfld, field);
currGetIl.Emit(OpCodes.Ret);
var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + propertyName,
getSetAttr,
null,
new Type[] { propType });
new[] { propType });
//store value in private field and set the isdirty flag
var currSetIL = currSetPropMthdBldr.GetILGenerator();
currSetIL.Emit(OpCodes.Ldarg_0);
currSetIL.Emit(OpCodes.Ldarg_1);
currSetIL.Emit(OpCodes.Stfld, field);
currSetIL.Emit(OpCodes.Ldarg_0);
currSetIL.Emit(OpCodes.Ldc_I4_1);
currSetIL.Emit(OpCodes.Call, setIsDirtyMethod);
currSetIL.Emit(OpCodes.Ret);
var currSetIl = currSetPropMthdBldr.GetILGenerator();
currSetIl.Emit(OpCodes.Ldarg_0);
currSetIl.Emit(OpCodes.Ldarg_1);
currSetIl.Emit(OpCodes.Stfld, field);
currSetIl.Emit(OpCodes.Ldarg_0);
currSetIl.Emit(OpCodes.Ldc_I4_1);
currSetIl.Emit(OpCodes.Call, setIsDirtyMethod);
currSetIl.Emit(OpCodes.Ret);
//TODO: Should copy all attributes defined by the interface?
if (isIdentity)
......@@ -478,7 +465,6 @@ private static void CreateProperty<T>(TypeBuilder typeBuilder, string propertyNa
typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod);
typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod);
}
}
}
......@@ -489,7 +475,10 @@ public TableAttribute(string tableName)
{
Name = tableName;
}
public string Name { get; private set; }
// ReSharper disable once MemberCanBePrivate.Global
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string Name { get; set; }
}
// do not want to depend on data annotations that is not in client profile
......@@ -514,43 +503,45 @@ public class ComputedAttribute : Attribute
}
}
public interface ISqlAdapter
public partial interface ISqlAdapter
{
int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert);
}
public class SqlServerAdapter : ISqlAdapter
public partial class SqlServerAdapter : ISqlAdapter
{
public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert)
{
string cmd = String.Format("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList);
var cmd = String.Format("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList);
connection.Execute(cmd, entityToInsert, transaction: transaction, commandTimeout: commandTimeout);
connection.Execute(cmd, entityToInsert, transaction, commandTimeout);
//NOTE: would prefer to use SCOPE_IDENTITY but this are not available on SQLCE
//NOTE: would prefer to use IDENT_CURRENT('tablename') or IDENT_SCOPE but these are not available on SQLCE
var r = connection.Query("select @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout);
int id = (int)r.First().id;
if (keyProperties.Any())
keyProperties.First().SetValue(entityToInsert, id, null);
var id = (int)r.First().id;
var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray();
if (propertyInfos.Any())
propertyInfos.First().SetValue(entityToInsert, id, null);
return id;
}
}
public class PostgresAdapter : ISqlAdapter
public partial class PostgresAdapter : ISqlAdapter
{
public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert)
{
StringBuilder sb = new StringBuilder();
var sb = new StringBuilder();
sb.AppendFormat("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList);
// If no primary key then safe to assume a join table with not too much data to return
if (!keyProperties.Any())
var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray();
if (!propertyInfos.Any())
sb.Append(" RETURNING *");
else
{
sb.Append(" RETURNING ");
bool first = true;
foreach (var property in keyProperties)
var first = true;
foreach (var property in propertyInfos)
{
if (!first)
sb.Append(", ");
......@@ -559,11 +550,11 @@ public int Insert(IDbConnection connection, IDbTransaction transaction, int? com
}
}
var results = connection.Query(sb.ToString(), entityToInsert, transaction: transaction, commandTimeout: commandTimeout);
var results = connection.Query(sb.ToString(), entityToInsert, transaction, commandTimeout: commandTimeout).ToList();
// Return the key by assinging the corresponding property in the object - by product is that it supports compound primary keys
int id = 0;
foreach (var p in keyProperties)
var id = 0;
foreach (var p in propertyInfos)
{
var value = ((IDictionary<string, object>)results.First())[p.Name.ToLower()];
p.SetValue(entityToInsert, value, null);
......@@ -574,21 +565,20 @@ public int Insert(IDbConnection connection, IDbTransaction transaction, int? com
}
}
public class SQLiteAdapter : ISqlAdapter
public partial class SQLiteAdapter : ISqlAdapter
{
public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, String tableName, string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert)
{
string cmd = String.Format("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList);
var cmd = String.Format("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList);
connection.Execute(cmd, entityToInsert, transaction: transaction, commandTimeout: commandTimeout);
connection.Execute(cmd, entityToInsert, transaction, commandTimeout);
var r = connection.Query("select last_insert_rowid() id", transaction: transaction, commandTimeout: commandTimeout);
int id = (int)r.First().id;
if (keyProperties.Any())
keyProperties.First().SetValue(entityToInsert, id, null);
var id = (int)r.First().id;
var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray();
if (propertyInfos.Any())
propertyInfos.First().SetValue(entityToInsert, id, null);
return id;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment