First let me apologize a bit for the length of this post, it's mostly code though so I hope you all bear with me!
I have a scenario in dealing with a legacy database, where I needed to write an IUserType using NHibernate 3.2 to take a 2 character "status" field and return a Boolean value from it. The status field can hold 3 possible values:
* 'DI' // 'Disabled', return false
* ' ' // blank or NULL, return true
* NULL
Here is what I have simplified.
Table Definition:
CREATE TABLE [dbo].[Client](
[clnID] [int] IDENTITY(1,1) NOT NULL,
[clnStatus] [char](2) NULL,
[clnComment] [varchar](250) NULL,
[clnDescription] [varchar](150) NULL,
[Version] [int] NOT NULL
)
Fluent Mapping:
public class ClientMapping : CoreEntityMapping<Client>
{
public ClientMapping()
{
SchemaAction.All().Table("Client");
LazyLoad();
Id(x => x.Id, "clnId").GeneratedBy.Identity();
Version(x => x.Version).Column("Version").Generated.Never().UnsavedValue("0").Not.Nullable();
OptimisticLock.Version();
Map(x => x.Comment, "clnComment").Length(250).Nullable();
Map(x => x.Description, "clnDescription").Length(250).Nullable();
Map(x => x.IsActive, "clnStatus").Nullable().CustomType<StatusToBoolType>();
}
}
My IUserType Implementation:
public class StatusToBoolType : IUserType
{
public bool IsMutable { get { return false; } }
public Type ReturnedType { get { return typeof(bool); } }
public SqlType[] SqlTypes { get { return new[] { NHibernateUtil.String.SqlType }; } }
public object DeepCopy(object value)
{
return value;
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return cached;
}
public object Disassemble(object value)
{
return value;
}
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.Equals(y);
}
public int GetHashCode(object x)
{
return x == null ? typeof(bool).GetHashCode() + 473 : x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (obj == null) return true;
var status = (string)obj;
if (status == " ") return true;
if (status == "DI") return false;
throw new Exception(string.Format("Expected data to be either empty or 'DI' but was '{0}'.", status));
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var parameter = ((IDataParameter) cmd.Parameters[index]);
var active = value == null || (bool) value;
if (active)
parameter.Value = " ";
else
parameter.Value = "DI";
}
}
However this doesn't work. This unit test fails with an inaccurate count.
[TestMethod]
public void GetAllActiveClientsTest()
{
//ACT
var count = Session.QueryOver<Client>()
.Where(x => x.IsActive)
.SelectList(l => l.SelectCount(x => x.Id))
.FutureValue<int>().Value;
//ASSERT
Assert.AreNotEqual(0, count);
Assert.AreEqual(1721, count);
}
The reason it fails is because it generates the following SQL:
SELECT count(this_.clnID) as y0_ FROM Client this_ WHERE this_.clnstatus = #p0;
/* #p0 = ' ' [Type: String (0)] */
But I need it to generate this instead:
SELECT count(this_.clnID) as y0_ FROM Client this_ WHERE (this_.clnstatus = #p0 <b> OR this_.clnstatus IS NULL);</b>
After some debugging I saw that the NullSafeSet() method in my StatusToBoolType class is invoked before the query is generated, so I was able to get around this by writing some hackish code in that method to manipulate the SQL in the cmd.CommandText property.
...
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var parameter = ((IDataParameter) cmd.Parameters[index]);
var active = value == null || (bool) value;
if (active)
{
parameter.Value = " ";
if (cmd.CommandText.ToUpper().StartsWith("SELECT") == false) return;
var paramindex = cmd.CommandText.IndexOf(parameter.ParameterName);
if (paramindex > 0)
{
// Purpose: change [columnName] = #p0 ==> ([columnName] = #p0 OR [columnName] IS NULL)
paramindex += parameter.ParameterName.Length;
var before = cmd.CommandText.Substring(0, paramindex);
var after = cmd.CommandText.Substring(paramindex);
//look at the text before the '= #p0' and find the column name...
var columnSection = before.Split(new[] {"= " + parameter.ParameterName}, StringSplitOptions.RemoveEmptyEntries).Reverse().First();
var column = columnSection.Substring(columnSection.Trim().LastIndexOf(' ')).Replace("(", "");
var myCommand = string.Format("({0} = {1} OR {0} IS NULL)", column.Trim(), parameter.ParameterName);
paramindex -= (parameter.ParameterName.Length + column.Length + 1);
var orig = before.Substring(0, paramindex);
cmd.CommandText = orig + myCommand + after;
}
}
else
parameter.Value = "DI";
}
But this is NHibernate!!! Hacking the sql statement like this can't possibly be the correct way to handle this? Right?
Because it is a shared legacy database, I can't change the table schema to NOT NULL otherwise I would have just done that, and avoided this scenario.
So finally after all this prelude my question is simply this, where can I tell NHibernate to generate a custom SQL criteria statement for this IUserType?
Thank you all in advance!
Solved it!
After I posted my question I went back to the drawing board, and I came up with a solution that doesn't require hacking the generated SQL in the IUserType implementation. In fact this solution doesn't need the IUserType at all!
Here is what I did.
First, I changed the IsActive column to use a formula to handle the null checking. This fixed my issue with the QueryOver failing, because now everytime NHibernate deals with IsActive property it injects my sql formula to handle null.
The downside to this approach was that after I put in the formula all of my save tests failed. It turns out that formula properties are effectively ReadOnly properties.
So to get around this issue, I added a protected property to the entity to hold the status value from the database.
Next, I changed the IsActive property to set the protected status property to " " or "DI". And finally I changed the FluentMapping to Reveal the protected Status property to NHibernate so that NHibernate can track it. Now that NHibernate is aware of Status it can include it on its INSERT/UPDATE statements.
I am going to include my solution below in case anyone else is interested.
Client class
public class Client
{
...
protected virtual string Status { get; set; }
private bool _isActive;
public virtual bool IsActive
{
get { return _isActive; }
set
{
_isActive = value;
Status = (_isActive) ? " " : "DI";
}
}
}
Changes to Fluent Mapping
public class ClientMapping : CoreEntityMapping<Client>
{
public ClientMapping()
{
....
Map(Reveal.Member<E>("Status"), colName).Length(2);
Map(x => x.IsActive).Formula("case when clnStatus is null then ' ' else clnStatus end");
}
}
Related
Hello I have SQL server with setting up always encrypted feature, also I setup EF for work with always encrypted columns, but when I try to add/update, for Db manipulation I use DbContext, entry in my Db I get follow error:
Operand type clash: decimal(1,0) encrypted with (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = '****', column_encryption_key_database_name = '****') is incompatible with decimal(6,2) encrypted with (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = '*****', column_encryption_key_database_name = '****')
Model that I use
public class Model
{
/// <summary>
/// Payment method name
/// </summary>
[Column(TypeName = "nvarchar(MAX)")]
public string Name { get; set; }
/// <summary>
/// Payment method description
/// </summary>
[Column(TypeName = "nvarchar(MAX)")]
public string Description { get; set; }
/// <summary>
/// Fee charges for using payment method
/// </summary>
[Column(TypeName = "decimal(6,2)")]
public decimal Fee { get; set; }
}
Also I tried to specify decimal format in OnModelCreating method
builder.Entity<Model>().Property(x => x.Fee).HasColumnType("decimal(6,2)");
What I missed ?
Thanks for any advice
My colleague and I have found a workaround to the problem using the DiagnosticSource.
You must know that:
Entity Framework Core hooks itself into DiagnosticSource.
DiagnosticSource uses the observer pattern to notify its observers.
The idea is to populate the 'Precision' and 'Scale' fields of the command object (created by EFCore), in this way the call made to Sql will contain all the information necessary to correctly execute the query.
First of all, create the listener:
namespace YOUR_NAMESPACE_HERE
{
public class EfGlobalListener : IObserver<DiagnosticListener>
{
private readonly CommandInterceptor _interceptor = new CommandInterceptor();
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
public void OnNext(DiagnosticListener value)
{
if (value.Name == DbLoggerCategory.Name)
{
value.Subscribe(_interceptor);
}
}
}
}
Where CommandInterceptor is:
namespace YOUR_NAMESPACE_HERE
{
public class CommandInterceptor : IObserver<KeyValuePair<string, object>>
{
// This snippet of code is only as example, you could maybe use Reflection to retrieve Field mapping instead of using Dictionary
private Dictionary<string, (byte Precision, byte Scale)> _tableMapping = new Dictionary<string, (byte Precision, byte Scale)>
{
{ "Table1.DecimalField1", (18, 2) },
{ "Table2.DecimalField1", (12, 6) },
{ "Table2.DecimalField2", (10, 4) },
};
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
public void OnNext(KeyValuePair<string, object> value)
{
if (value.Key == RelationalEventId.CommandExecuting.Name)
{
// After that EF Core generates the command to send to the DB, this method will be called
// Cast command object
var command = ((CommandEventData)value.Value).Command;
// command.CommandText -> contains SQL command string
// command.Parameters -> contains all params used in sql command
// ONLY FOR EXAMPLE PURPOSES
// This code may contain errors.
// It was written only as an example.
string table = null;
string[] columns = null;
string[] parameters = null;
var regex = new Regex(#"^INSERT INTO \[(.+)\] \((.*)\)|^VALUES \((.*)\)|UPDATE \[(.*)\] SET (.*)$", RegexOptions.Multiline);
var matches = regex.Matches(command.CommandText);
foreach (Match match in matches)
{
if(match.Groups[1].Success)
{
// INSERT - TABLE NAME
table = match.Groups[1].Value;
}
if (match.Groups[2].Success)
{
// INSERT - COLS NAMES
columns = match.Groups[2].Value.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Replace("[", string.Empty).Replace("]", string.Empty).Trim()).ToArray();
}
if (match.Groups[3].Success)
{
// INSERT - PARAMS VALUES
parameters = match.Groups[3].Value.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).ToArray();
}
if (match.Groups[4].Success)
{
// UPDATE - TABLE NAME
table = match.Groups[4].Value;
}
if (match.Groups[5].Success)
{
// UPDATE - COLS/PARAMS NAMES/VALUES
var colParams = match.Groups[5].Value.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(p => p.Replace("[", string.Empty).Replace("]", string.Empty).Trim()).ToArray();
columns = colParams.Select(cp => cp.Split('=', StringSplitOptions.RemoveEmptyEntries)[0].Trim()).ToArray();
parameters = colParams.Select(cp => cp.Split('=', StringSplitOptions.RemoveEmptyEntries)[1].Trim()).ToArray();
}
}
// After taking all the necessary information from the sql command
// we can add Precision and Scale to all decimal parameters
foreach (var item in command.Parameters.OfType<SqlParameter>().Where(p => p.DbType == DbType.Decimal))
{
var index = Array.IndexOf<string>(parameters, item.ParameterName);
var columnName = columns.ElementAt(index);
var key = $"{table}.{columnName}";
// Add Precision and Scale, that fix our problems w/ always encrypted columns
item.Precision = _tableMapping[key].Precision;
item.Scale = _tableMapping[key].Scale;
}
}
}
}
}
Finally add in the Startup.cs the following line of code to register the listener:
DiagnosticListener.AllListeners.Subscribe(new EfGlobalListener());
Ecountered the same issue.
Adjusted #SteeBono interceptor to work with commands which contain multiple statements:
public class AlwaysEncryptedDecimalParameterInterceptor : DbCommandInterceptor, IObserver<KeyValuePair<string, object>>
{
private Dictionary<string, (SqlDbType DataType, byte? Precision, byte? Scale)> _decimalColumnSettings =
new Dictionary<string, (SqlDbType DataType, byte? Precision, byte? Scale)>
{
// MyTableDecimal
{ $"{nameof(MyTableDecimal)}.{nameof(MyTableDecimal.MyDecimalColumn)}", (SqlDbType.Decimal, 18, 6) },
// MyTableMoney
{ $"{nameof(MyTableMoney)}.{nameof(MyTableMoney.MyMoneyColumn)}", (SqlDbType.Money, null, null) },
};
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
// After that EF Core generates the command to send to the DB, this method will be called
public void OnNext(KeyValuePair<string, object> value)
{
if (value.Key == RelationalEventId.CommandExecuting.Name)
{
System.Data.Common.DbCommand command = ((CommandEventData)value.Value).Command;
Regex regex = new Regex(#"INSERT INTO \[(.+)\] \((.*)\)(\r\n|\r|\n)+VALUES \(([^;]*)\);|UPDATE \[(.*)\] SET (.*)|MERGE \[(.+)\] USING \((\r\n|\r|\n)+VALUES \(([^A]*)\) AS \w* \((.*)\)");
MatchCollection matches = regex.Matches(command.CommandText);
foreach (Match match in matches)
{
(string TableName, string[] Columns, string[] Params) commandComponents = GetCommandComponents(match);
int countOfColumns = commandComponents.Columns.Length;
// After taking all the necessary information from the sql command
// we can add Precision and Scale to all decimal parameters and set type for Money ones
for (int index = 0; index < commandComponents.Params.Length; index++)
{
SqlParameter decimalSqlParameter = command.Parameters.OfType<SqlParameter>()
.FirstOrDefault(p => commandComponents.Params[index] == p.ParameterName);
if (decimalSqlParameter == null)
{
continue;
}
string columnName = commandComponents.Columns.ElementAt(index % countOfColumns);
string settingKey = $"{commandComponents.TableName}.{columnName}";
if (_decimalColumnSettings.ContainsKey(settingKey))
{
(SqlDbType DataType, byte? Precision, byte? Scale) settings = _decimalColumnSettings[settingKey];
decimalSqlParameter.SqlDbType = settings.DataType;
if (settings.Precision.HasValue)
{
decimalSqlParameter.Precision = settings.Precision.Value;
}
if (settings.Scale.HasValue)
{
decimalSqlParameter.Scale = settings.Scale.Value;
}
}
}
}
}
}
private (string TableName, string[] Columns, string[] Params) GetCommandComponents(Match match)
{
string tableName = null;
string[] columns = null;
string[] parameters = null;
// INSERT
if (match.Groups[1].Success)
{
tableName = match.Groups[1].Value;
columns = match.Groups[2].Value.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Replace("[", string.Empty)
.Replace("]", string.Empty)
.Trim()).ToArray();
parameters = match.Groups[4].Value
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Trim()
.Replace($"),{Environment.NewLine}(", string.Empty)
.Replace("(", string.Empty)
.Replace(")", string.Empty))
.ToArray();
return (
TableName: tableName,
Columns: columns,
Params: parameters);
}
// UPDATE
if (match.Groups[5].Success)
{
tableName = match.Groups[5].Value;
string[] colParams = match.Groups[6].Value.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Replace("[", string.Empty).Replace("]", string.Empty).Trim())
.ToArray();
columns = colParams.Select(cp => cp.Split('=', StringSplitOptions.RemoveEmptyEntries)[0].Trim()).ToArray();
parameters = colParams.Select(cp => cp.Split('=', StringSplitOptions.RemoveEmptyEntries)[1].Trim()).ToArray();
return (
TableName: tableName,
Columns: columns,
Params: parameters);
}
// MERGE
if (match.Groups[7].Success)
{
tableName = match.Groups[7].Value;
parameters = match.Groups[9].Value.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Trim()
.Replace($"),{Environment.NewLine}(", string.Empty)
.Replace("(", string.Empty)
.Replace(")", string.Empty))
.ToArray();
columns = match.Groups[10].Value.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Replace("[", string.Empty).Replace("]", string.Empty).Trim()).ToArray();
return (
TableName: tableName,
Columns: columns,
Params: parameters);
}
throw new Exception($"{nameof(AlwaysEncryptedDecimalParameterInterceptor)} was not able to parse the command");
}
}
public bool IsList(object value)
{
Type type = value.GetType();
// Check if type is a generic list of any type
}
What's the best way to check if the given object is a list, or can be cast to a list?
For you guys that enjoy the use of extension methods:
public static bool IsGenericList(this object o)
{
var oType = o.GetType();
return (oType.IsGenericType && (oType.GetGenericTypeDefinition() == typeof(List<>)));
}
So, we could do:
if(o.IsGenericList())
{
//...
}
using System.Collections;
if(value is IList && value.GetType().IsGenericType) {
}
bool isList = o.GetType().IsGenericType
&& o.GetType().GetGenericTypeDefinition() == typeof(IList<>));
public bool IsList(object value) {
return value is IList
|| IsGenericList(value);
}
public bool IsGenericList(object value) {
var type = value.GetType();
return type.IsGenericType
&& typeof(List<>) == type.GetGenericTypeDefinition();
}
Here's an implementation that works in .NET Standard, and works against interfaces:
public static bool ImplementsGenericInterface(this Type type, Type interfaceType)
{
return type
.GetTypeInfo()
.ImplementedInterfaces
.Any(x => x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == interfaceType);
}
And here are the tests (xunit):
[Fact]
public void ImplementsGenericInterface_List_IsValidInterfaceTypes()
{
var list = new List<string>();
Assert.True(list.GetType().ImplementsGenericInterface(typeof(IList<>)));
Assert.True(list.GetType().ImplementsGenericInterface(typeof(IEnumerable<>)));
Assert.True(list.GetType().ImplementsGenericInterface(typeof(IReadOnlyList<>)));
}
[Fact]
public void ImplementsGenericInterface_List_IsNotInvalidInterfaceTypes()
{
var list = new List<string>();
Assert.False(list.GetType().ImplementsGenericInterface(typeof(string)));
Assert.False(list.GetType().ImplementsGenericInterface(typeof(IDictionary<,>)));
Assert.False(list.GetType().ImplementsGenericInterface(typeof(IComparable<>)));
Assert.False(list.GetType().ImplementsGenericInterface(typeof(DateTime)));
}
if(value is IList && value.GetType().GetGenericArguments().Length > 0)
{
}
Based on Victor Rodrigues' answer, we can devise another method for generics. In fact, the original solution can be reduced to only two lines:
public static bool IsGenericList(this object Value)
{
var t = Value.GetType();
return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<>);
}
public static bool IsGenericList<T>(this object Value)
{
var t = Value.GetType();
return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<T>);
}
I'm using the following code:
public bool IsList(Type type) => type.IsGenericType && (
(type.GetGenericTypeDefinition() == typeof(List<>))
|| (type.GetGenericTypeDefinition() == typeof(IList<>))
);
Probably the best way would be to do something like this:
IList list = value as IList;
if (list != null)
{
// use list in here
}
This will give you maximum flexibility and also allow you to work with many different types that implement the IList interface.
I am creating an application with MVC4 and entity framework 5. How do can I implement this?
I have looked around and found that I need to override SaveChanges .
Does anyone have any sample code on this? I am using code first approach.
As an example, the way I am saving data is as follows,
public class AuditZoneRepository : IAuditZoneRepository
{
private AISDbContext context = new AISDbContext();
public int Save(AuditZone model, ModelStateDictionary modelState)
{
if (model.Id == 0)
{
context.AuditZones.Add(model);
}
else
{
var recordToUpdate = context.AuditZones.FirstOrDefault(x => x.Id == model.Id);
if (recordToUpdate != null)
{
recordToUpdate.Description = model.Description;
recordToUpdate.Valid = model.Valid;
recordToUpdate.ModifiedDate = DateTime.Now;
}
}
try
{
context.SaveChanges();
return 1;
}
catch (Exception ex)
{
modelState.AddModelError("", "Database error has occured. Please try again later");
return -1;
}
}
}
There is no need to override SaveChanges.
You can
Trigger Context.ChangeTracker.DetectChanges(); // may be necessary depending on your Proxy approach
Then analyze the context BEFORE save.
you can then... add the Change Log to the CURRENT Unit of work.
So the log gets saved in one COMMIT transaction.
Or process it as you see fit.
But saving your change log at same time. makes sure it is ONE Transaction.
Analyzing the context sample:
I have a simple tool, to Dump context content to debug output so when in debugger I can use immediate window to check content. eg
You can use this as a starter to prepare your CHANGE Log.
Try it in debugger immediate window. I have FULL dump on my Context class.
Sample Immediate window call. UoW.Context.FullDump();
public void FullDump()
{
Debug.WriteLine("=====Begin of Context Dump=======");
var dbsetList = this.ChangeTracker.Entries();
foreach (var dbEntityEntry in dbsetList)
{
Debug.WriteLine(dbEntityEntry.Entity.GetType().Name + " => " + dbEntityEntry.State);
switch (dbEntityEntry.State)
{
case EntityState.Detached:
case EntityState.Unchanged:
case EntityState.Added:
case EntityState.Modified:
WriteCurrentValues(dbEntityEntry);
break;
case EntityState.Deleted:
WriteOriginalValues(dbEntityEntry);
break;
default:
throw new ArgumentOutOfRangeException();
}
Debug.WriteLine("==========End of Entity======");
}
Debug.WriteLine("==========End of Context======");
}
private static void WriteCurrentValues(DbEntityEntry dbEntityEntry)
{
foreach (var cv in dbEntityEntry.CurrentValues.PropertyNames)
{
Debug.WriteLine(cv + "=" + dbEntityEntry.CurrentValues[cv]);
}
}
private static void WriteOriginalValues(DbEntityEntry dbEntityEntry)
{
foreach (var cv in dbEntityEntry.OriginalValues.PropertyNames)
{
Debug.WriteLine(cv + "=" + dbEntityEntry.OriginalValues[cv]);
}
}
}
EDIT: Get the changes
I use this routine to get chnages...
public class ObjectPair {
public string Key { get; set; }
public object Original { get; set; }
public object Current { get; set; }
}
public virtual IList<ObjectPair> GetChanges(object poco) {
var changes = new List<ObjectPair>();
var thePoco = (TPoco) poco;
foreach (var propName in Entry(thePoco).CurrentValues.PropertyNames) {
var curr = Entry(thePoco).CurrentValues[propName];
var orig = Entry(thePoco).OriginalValues[propName];
if (curr != null && orig != null) {
if (curr.Equals(orig)) {
continue;
}
}
if (curr == null && orig == null) {
continue;
}
var aChangePair = new ObjectPair {Key = propName, Current = curr, Original = orig};
changes.Add(aChangePair);
}
return changes;
}
edit 2 If you must use the Internal Object tracking.
var context = ???// YOUR DBCONTEXT class
// get objectcontext from dbcontext...
var objectContext = ((IObjectContextAdapter) context).ObjectContext;
// for each tracked entry
foreach (var dbEntityEntry in context.ChangeTracker.Entries()) {
//get the state entry from the statemanager per changed object
var stateEntry = objectContext.ObjectStateManager.GetObjectStateEntry(dbEntityEntry.Entity);
var modProps = stateEntry.GetModifiedProperties();
Debug.WriteLine(modProps.ToString());
}
I decompiled EF6 . Get modified is indeed using private bit array to track fields that have
been changed.
// EF decompiled source..... _modifiedFields is a bitarray
public override IEnumerable<string> GetModifiedProperties()
{
this.ValidateState();
if (EntityState.Modified == this.State && this._modifiedFields != null)
{
for (int i = 0; i < this._modifiedFields.Length; ++i)
{
if (this._modifiedFields[i])
yield return this.GetCLayerName(i, this._cacheTypeMetadata);
}
}
}
I'm a new user in LINQ to SQL and I have some problems using it.
I've used LINQ to SQL Designer and I have created my classes, mapped on the DB tables.
In particular, I have one class, named voice:
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.voce")]
public partial class voce : INotifyPropertyChanging, INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
private int _id_voce;
... other private fields;
private int _category;
private EntityRef<category> _category1;
public voce()
{
this._riepilogo = new EntitySet<riepilogo>(new Action<riepilogo>(this.attach_riepilogo), new Action<riepilogo>(this.detach_riepilogo));
this._hera = default(EntityRef<hera>);
this._category1 = default(EntityRef<category>);
OnCreated();
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_id_voce", AutoSync=AutoSync.OnInsert, DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)]
public int id_voce
{
get
{
return this._id_voce;
}
set
{
if ((this._id_voce != value))
{
this.Onid_voceChanging(value);
this.SendPropertyChanging();
this._id_voce = value;
this.SendPropertyChanged("id_voce");
this.Onid_voceChanged();
}
}
}
......
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_category", DbType="Int NOT NULL")]
public int category
{
get
{
return this._category;
}
set
{
if ((this._category != value))
{
if (this._category1.HasLoadedOrAssignedValue)
{
throw new System.Data.Linq.ForeignKeyReferenceAlreadyHasValueException();
}
this.OncategoryChanging(value);
this.SendPropertyChanging();
this._category = value;
this.SendPropertyChanged("category");
this.OncategoryChanged();
}
}
}
As you can see, voce class has a field named category that refers to a table named category.
When I add a new voce to my database, I create a new voce istance and, using the DataContext, i simply add it, using:
voce v = new voce(){...field, category1 = //create or retrieve category};
In particular, the category field is retrieved from the DB if already exists or, if not, it is inserted, before I insert the voice.
The problem is that when I add the voice in the database:
datacontext.InsertOnSubmit(v);
datacontext.SubmitChanges();
it inserts the category again, failing with the unique contraint.
How can I add a voice without adding every nested object?
Thank you and sorry for my bad English.
internal category GetCategoryFromDescription (string desc, Utility.VOICE_MODALITY mode)
{
bool type = mode == Utility.VOICE_MODALITY.ENTRATA ? true : false;
var query = from cat in dc.category
where cat.description == desc && cat.type == type
select cat;
if (query.Count() == 0)
{
category newC = new category() { description = desc };
dc.category.InsertOnSubmit(newC);
dc.SubmitChanges();
return newC;
}
else
return query.Single();
}
I am trying to create an NHibernate IUserType for the Noda Time LocalTime type which would logically map to a time type in Sql Server 2008/2012. I am able to get values saving and loading from the database. However, I can't write queries involving comparison of local times like _session.Query<SchedulingTemplate>().Where(x => x.Start < end && x.End >= start) gives the error SqlException (0x80131904): The data types time and datetime are incompatible in the less than operator.
The relevant code from my user type is:
public Type ReturnedType
{
get { return typeof(LocalTime); }
}
public override object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var dbValue = NHibernateUtil.Time.NullSafeGet(rs, names);
if(dbValue == null)
return null;
return LocalDateTime.FromDateTime((DateTime)dbValue).TimeOfDay;
}
public override void NullSafeSet(IDbCommand cmd, object value, int index)
{
if(value == null)
NHibernateUtil.Time.NullSafeSet(cmd, null, index);
else
NHibernateUtil.Time.NullSafeSet(cmd, ((LocalTime)value).LocalDateTime.ToDateTimeUnspecified(), index);
}
public override SqlType[] SqlTypes
{
get { return new[] { SqlTypeFactory.Time }; }
}
The problem is that despite the above code indicating the database type is a time, it generates the following query (per Sql Profiler):
exec sp_executesql N'select [...] from [SchedulingTemplate] scheduling0_ where scheduling0_.Start<#p0 and scheduling0_.[End]>=#p1',N'#p0 datetime,#p1 datetime',#p0='1753-01-01 20:00:00',#p1='1753-01-01 06:00:00'
(note I omitted the select list for brevity)
Notice that the type and value of the parameters is being treated as datetime.
This appears to be very similar to two NH bugs that have been closed https://nhibernate.jira.com/browse/NH-2661 and https://nhibernate.jira.com/browse/NH-2660.
I tried to use NHibernateUtil.TimeAsTimeSpan and that didn't seem to work either. It generated exactly the same query which surprised me. I am thinking maybe the issue described in NH-2661 also exists for user types and was not fixed for that?
I am using NHibernate v3.3.1.400 and Noda Time 1.0.0-beta2
Following #Firo's advice, I worked from the time SqlType and came up with this:
using NHibernate;
using NHibernate.Dialect;
using NHibernate.SqlTypes;
using NHibernate.Type;
using NodaTime;
using NodaTime.Text;
using System;
using System.Data;
using System.Data.SqlClient;
[Serializable]
public class LocalTimeType : PrimitiveType, IIdentifierType
{
private readonly LocalTimePattern _timePattern = LocalTimePattern.CreateWithInvariantCulture("h:mm:ss tt");
public LocalTimeType() : base(SqlTypeFactory.Time) { }
public override string Name
{
get { return "LocalTime"; }
}
public override object Get(IDataReader rs, int index)
{
try
{
if (rs[index] is TimeSpan) //For those dialects where DbType.Time means TimeSpan.
{
var time = (TimeSpan)rs[index];
return LocalTime.Midnight + Period.FromTicks(time.Ticks);
}
var dbValue = Convert.ToDateTime(rs[index]);
return LocalDateTime.FromDateTime(dbValue).TimeOfDay;
}
catch (Exception ex)
{
throw new FormatException(string.Format("Input string '{0}' was not in the correct format.", rs[index]), ex);
}
}
public override object Get(IDataReader rs, string name)
{
return Get(rs, rs.GetOrdinal(name));
}
public override Type ReturnedClass
{
get { return typeof(LocalTime); }
}
public override void Set(IDbCommand st, object value, int index)
{
var parameter = ((SqlParameter)st.Parameters[index]);
parameter.SqlDbType = SqlDbType.Time; // HACK work around bad behavior, M$ says not ideal, but as intended, NH says this is a bug in MS may work around eventually
parameter.Value = new TimeSpan(((LocalTime)value).TickOfDay);
}
public override bool IsEqual(object x, object y)
{
return Equals(x, y);
}
public override int GetHashCode(object x, EntityMode entityMode)
{
return x.GetHashCode();
}
public override string ToString(object val)
{
return _timePattern.Format((LocalTime)val);
}
public object StringToObject(string xml)
{
return string.IsNullOrEmpty(xml) ? null : FromStringValue(xml);
}
public override object FromStringValue(string xml)
{
return _timePattern.Parse(xml).Value;
}
public override Type PrimitiveClass
{
get { return typeof(LocalTime); }
}
public override object DefaultValue
{
get { return new LocalTime(); }
}
public override string ObjectToSQLString(object value, Dialect dialect)
{
return "'" + _timePattern.Format((LocalTime)value) + "'";
}
}
The key code is in the Set method where is says:
var parameter = ((SqlParameter)st.Parameters[index]);
parameter.SqlDbType = SqlDbType.Time;
This is needed because the MS data provider takes setting the DbType to DbType.Time to mean the underlying type should be DateTime. You must set the SqlDbType to time for it to work.