TL/DR - Is there a way to force EF Core to allow me to update a Discriminator column?
I'm playing around with Entity Framework Core for the first time and trying to implement a simple system where I have Clients of 3 types to manage. Two of these types are only meaningfully different in their type value itself (type1 and type2). The third type (special) just has an extended set of possible relationships.
So, in the database I created a single table to store the core data:
CREATE TABLE [Clients].[Clients](
[ClientId] [varchar](30) NOT NULL,
[ClientName] [varchar](100) NOT NULL,
[ProtocolType] [varchar](30) NOT NULL,
[ClientLogoUrl] [varchar](max) NULL,
CONSTRAINT [PK_Clients_Clients] PRIMARY KEY ([ClientId]),
CONSTRAINT [UQ_Clients_Client_Names] UNIQUE ([ClientName]),
CONSTRAINT [UQ_Clients_Client_Protocol_XRef] UNIQUE ([ClientId],[ProtocolType]),
CONSTRAINT [FK_Clients_Client_Protocol] FOREIGN KEY([ProtocolType])
REFERENCES [Clients].[Protocols] ([ProtocolType])
)
In my code I then created these models:
public class Client
{
public string ClientId { get; set; }
public string ClientName { get; set; }
public string ProtocolType { get; set; }
public string ClientLogoUrl { get; set; }
public Protocol Protocol { get; set; }
}
And for the one subtype of Clients that have more possible relationships:
public class SpecialClient : Client
{
public List<Service> Services { get; set; }
//And more
}
I initially had this mapped in OnModelCreating as:
modelBuilder.Entity<Clients.Client>()
.HasDiscriminator<string>("ProtocolType")
.HasValue<Clients.SpecialClient>("special");
But this ended up querying the database only for protocol types special and Client - not what I wanted. I'd assumed EF would just use the base class for "all other values" but it did not. So then I tried:
modelBuilder.Entity<Clients.Client>()
.HasDiscriminator<string>("ProtocolType")
.HasValue<Clients.SpecialClient>("special")
.HasValue<Clients.Client>("type1")
.HasValue<Clients.Client>("type2");
But this effectively only took the last supplied value and would only query the database for special and type2. So, I finally accepted that I'd need to introduce a redundant extra class with no extra functionality just so that I could make the mapper happy:
public class Type2Client : Client {}
And:
modelBuilder.Entity<Clients.Client>()
.HasDiscriminator<string>("ProtocolType")
.HasValue<Clients.SpecialClient>("special")
.HasValue<Clients.Client>("type1")
.HasValue<Clients.Type2Client>("type2");
Finally - EF is happy, all is good, I can move on with my life. However, the next challenge is, clients can change protocols.
So, I load a client. I change its protocol type. I save it:
_context.Update(client);
await _context.SaveChangesAsync();
And EF generates this SQL:
SET NOCOUNT ON;
UPDATE [Clients].[Clients] SET [ClientLogoUrl] = #p0, [ClientName] = #p1
WHERE [ClientId] = #p2;
SELECT ##ROWCOUNT;
Harumph. No SET for the ProtocolType column. Is there some cunning way to make this work?
You need to change default "after save" behavior of discriminator column. By default, discriminator column has AfterSaveBehavior set to Throw. That means that if explicit value is set, or changed - exception is thrown when trying to save changes. That also means Update will not mark such property as modified, because that will lead to exception during SaveChanges anyway.
So in your case change save behavior to Save for discriminator column (do that after you made this column discriminator of course):
modelBuilder.Entity<Client>()
.Property(c => c.ProtocolType).Metadata.AfterSaveBehavior = PropertySaveBehavior.Save;
And it should work as you expect.
Related
I have a problem with the SQLite in-memory database. The normal database is working.
This is my model code
public class Log
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string Message { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime CreatedAt { get; set; }
}
The SQL statement to create the table
CREATE TABLE [dbo].[AuditLogs]
(
[Id] UNIQUEIDENTIFIER NOT NULL DEFAULT newid() PRIMARY KEY,
[Message] varchar(max) NOT NULL CONSTRAINT ensure_json CHECK (ISJSON([Message])> 0),
[CreatedAt] datetime NOT NULL default GetDate()
)
The error
Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'NOT NULL constraint failed: AuditLogs.CreatedAt'.
Do you have any solution?
You could just use this:
public Guid Id { get; set; } = Guid.NewGuid();
The problem with the Entity Framework is that it won't generate keys for you. If it is database-generated then some trigger in the database would still need to create this ID. This is generally done when the ID is of type int, but not Guid. Then again, SQLite is a weird database provider to begin with as it doesn't really has datatypes. Data type definitions are more suggestions and not enforced by the engine. (But EF will enforce it.)
Anyways, since you use Guids there's nothing wrong with assigning new values to the property, as they will be overwritten by the value in the database on retrieval. But SQLite isn't really generating values for you.
Also, I would use public DateTime CreatedAt { get; set; } = DateTime.Now(); for the same reason. I myself actually had similar problems but I use the Fluid API instead and use this:
var hostBuilder = modelBuilder.Entity<Host>();
hostBuilder
.Property(r => r.Id)
.HasColumnOrder(0)
.IsRequired()
.HasColumnName("Key")
.HasColumnType("varchar(36)")
.HasComment($"Primary key");
hostBuilder
.Property<DateTime>("Created")
.HasColumnOrder(1)
.HasDefaultValueSql("CURRENT_TIMESTAMP")
.ValueGeneratedOnAdd()
.HasComment($"When was it created?");
hostBuilder
.HasKey(r => r.Id)
.HasName($"PK_Visitor_Host_Key");
And my class only has the Id property defined, as I don't need the Created field in my project. It still gets added, though! The HasDefaultValueSql() call will tell that the field is database-generated, including how it's generated. You might want to look into this Fluid API for your project. I prefer it over those attributes as it provides more options and better control, plus I can add fields to tables that are not important for my code, yet still required for other purposes...
(Btw. You don't want timestamps to be unique as two records could be created at exactly the same timestamp on fast systems.)
I have a simple model
[Table("InterfaceType")]
public class InterfaceType
{
[Key]
public int InterfaceTypeId { get; set; }
public string Description { get; set; }
}
and in my DbContext
public DbSet<InterfaceType> InterfaceTypes { get; set; }
and in my controller
List<InterfaceType> types = _context.InterfaceTypes.FromSql(
"SELECT * FROM [Interfaces].[Control].[InterfaceType]").ToList();
Which is returning the error:
InvalidOperationException: The required column 'InterfaceID' was not present in the results of a 'FromSql' operation.
I am using FromSql in other methods similar to this with no issue although those models do contain an InterfaceId. Why does this operation expect an InterfaceId when it is not in the model. I have also tried the below with the same result.
List<InterfaceType> types = _context.InterfaceTypes.FromSql(
"SELECT InterfaceTypeId, Description FROM [Interfaces].[Control].[InterfaceType]").ToList();
I have also tried:
interfacesOverview.SelectedInterface.InterfaceTypes = _context.InterfaceTypes.ToList();
After declaring via the fluent api:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<InterfaceType>().ToTable("InterfaceType", "Control");
}
with the same result.
For clarity here is the table in MSSQL:
CREATE TABLE [Control].[InterfaceType](
[InterfaceTypeId] [tinyint] NOT NULL,
[Description] [varchar](25) NULL,
CONSTRAINT [PK_InterfaceType] PRIMARY KEY CLUSTERED
(
[InterfaceTypeId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
UPDATE
I've looked at the SQL that EF is generating:
SELECT [i].[InterfaceTypeId], [i].[Description], [i].[InterfaceID] FROM [Control].[InterfaceType] AS [i]
Where is it getting InterfaceID from?
Where is it getting InterfaceID from?
First, it should be clear that it's not coming from the shown "simple" (but apparently incomplete) model.
The EF generated SQL clearly indicates that you didn't rename the PK property generated column, also there is no Discriminator column, so it cannot be coming from inheritance. And the chance that you have explicitly defined a shadow property called InterfaceID and not noticing it is small.
All this, along with the fact that the name InterfaceID matches one of the EF Core conventional names for FK property/column name for me is a clear indication of a conventional FK introduced by a relationship. For instance having a second model like this:
public class Interface
{
public int ID { get; set; }
// or
// public int InterfaceID { get; set; }
public ICollection<InterfaceType> InterfaceTypes { get; set; }
}
As explained in the Relationships - Single Navigation Property EF Core documentation topic:
Including just one navigation property (no inverse navigation, and no foreign key property) is enough to have a relationship defined by convention.
and the accompanying example shows Blog / Post model with only public List<Post> Posts { get; set; } property in Blog highlighted.
All EF Core runtime behaviors are based on model metadata. It doesn't matter what is the structure of your database, the more important is what EF Core thinks it is base on your model classes, data annotations and fluent configuration, and if that matches the database schema. The easier way to check that is to generate migration and check if it matches the database schema or not.
So if the relationship is intentional, then you have to update your database to match your model. Otherwise you need to update your model to match the database - by removing or ignoring the collection navigation property (or correcting the invalid data annotation / fluent configuration causing the discrepancy).
My understanding of this problem, is that EF created a Shadow Property
inside your model class, possibly by partially discovered relationship in your Interface model.
Also I feel there is a mismatch between your ModelSnapshot used by EFCore and real state of tables in Database (possibly by pending migration). Double check, how your InterfaceType in <YourDbContext>ModelSnapshot.cs, and check if there's a property you are missing.
My guess is that you also have an "Interface" table registered in the context that holds a reference to the InterfaceType. Interface would have an InterfaceTypeId field declared, however with EF, if you are using HasOne with a ForeignKey, check that you haven't accidentally assigned something like:
.HasOne(x => x.InterfaceType).WithOne().HasForeignKey<InterfaceType>("InterfaceId");
In the case of an Interface having an InterfaceType it would be mapped more like:
.HasOne(x => x.InterfaceType).WithMany();
This might have crept into one of your other associated entities. Often these are typos where the autocomplete picked the wrong type without you noticing. If that mapping exists on any of your classes, EF will be expecting to find an InterfaceId column on InterfaceType. Do a search on HasForeignKey<InterfaceType> and see if that turns up anything out of the ordinary.
First why not use
List<InterfaceType> types = _context.InterfaceTypes.ToList();
Secondly did you apply any changes to the model and forget to persist this to the database, as it could be that the column is correct in your class but not in your database. This is often something i forget to do when using a Code-FirstModel.
Here is some additional info on FromSQL :- https://learn.microsoft.com/en-us/ef/core/querying/raw-sql
More detail on migration here:- https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/
I hope this helps.
Maybe try to add DatabaseGeneratedAttribute
[Table("InterfaceType")]
public class InterfaceType
{
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity),Key()]
public int InterfaceTypeId { get; set; }
...
Just to check if there is a way I can reproduce, I created a sample .NET Core Console application to check this and in my case, I am able to retrieve the data from DB without any exception.
I understand you have other models where the same code is working,
and if you move the problematic code outside your original solution, you might be
able to figure out if there is something obvious you are missing.
I tried to follow your code as closely as possible in attempt to reproduce this issue, where some of the things I had to change.
I don't know which .NET Core and EF Core versions you have used. In my sample, I used:
.NET Core 2.2
Microsoft.EntityFrameworkCore 2.2
Microsoft.EntityFrameworkCore.Design 2.2
Microsoft.EntityFrameworkCore.SqlServer 2.2
Microsoft.EntityFrameworkCore.Tools 2.2
I created:
Model Class as per your sample
Context Class with OnModelCreating per your sample
Executed following dotnet core commands in the same order as listed below:
dotnet restore
dotnet build
dotnet ef migrations add InitMgr
dotnet ef database update
Added few test records in the table
Copied your records retrieval code, removed "[Interfaces]" from the query and debugged the code below.
var _context = new InterfaceTypeContext ();
List<InterfaceType> types = _context.InterfaceTypes.FromSql ("SELECT * FROM [Control].[InterfaceType]").ToList ();
I was able to retrieve the data from DB.
It would also help if you share Minimal, Complete, and Verifiable
example for someone to debug and help you find a solution for
this.
The following has worked for me:
Insert some data:
insert into [Control].[InterfaceType] values (1, 'Desc1'), (2, 'Desc2');
C#:
class SOContext : DbContext
{
public DbSet<InterfaceType> InterfaceTypes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var conn_string = #"Server=(localdb)\mssqllocaldb;Database=Interfaces;Trusted_Connection=Yes;";
optionsBuilder.UseSqlServer(conn_string);
}
}
[Table("InterfaceType", Schema = "Control")]
public class InterfaceType
{
[DatabaseGenerated(DatabaseGeneratedOption.None), Key]
public byte InterfaceTypeId { get; set; }
public string Description { get; set; }
public override string ToString() =>
$"Id: {InterfaceTypeId} | Description: {Description}";
}
// Output:
// Id: 1 | Description: Desc1
// Id: 2 | Description: Desc2
This could be a duplicate question but a lot of searching for the words in the title only got me a lot of unrelated results.
I have an entity that's roughly set up like this:
public abstract class A
{
public GUID AId { get; set; }
public string SomeProperty { get; set; }
}
public class B : A
{
public string SomeOtherProperty { get; set; }
}
The context has public DbSet<B> BInstances { get; set; } for B objects. In OnModelCreating, the mapping has A set to ignored and B is mapped to a table called TableB.
The AId field is not auto-generated (not an identity field) but it's set to be primary key, both in the database and in the mapping. In the database, the field is defined as a non-null uniqueidentifier with no default.
At runtime, I'm loading an instance of B using its key (_token is just a CancellationToken):
var b = await (dbCtx.BInstances.FirstOrDefaultAsync(e => e.AId), _token));
Then, a property of b is set and I try to save it back to database:
b.SomeOtherProperty = "some new text";
await (dbCtx.SaveChangesAsync(_token));
At this point, I'm getting a Violation of PRIMARY KEY constraint error from the database, stating that the value of AId cannot be inserted because it'd be a duplicate. Of course, the ID is already in the database, I loaded the entity from there, using the ID. For some reason, EF generates an INSERT statement, not an UPDATE and I don't understand why.
When I check dbCtx.Entry(b).State, it's already set to EntityState.Modified. I'm at a loss - can someone point out what I'm doing wrong? I never had issues with updating entities before but I haven't used EF with GUID primary keys (usually I use long primary keys).
I'm using EF 6 and .NET Framework 4.7.1.
Thank you all for the suggestions - this turned out to be a mapping problem that I caused.
In my OnModelCreating() call, I called MapInheritedProperties() on a type that didn't inherit from a base class (other than object, of course) - this seems to have triggered a problem. Other entities that do share a base class worked fine with the mapping call.
I also called ToTable() directly against the entity class - this broke my table mapping for reasons I do not understand. Once I moved that call inside Map(), it started working as expected.
So I went from this:
entity.ToTable("tablename");
to this:
entity.Map(m => m.ToTable("tablename"));
to solve the problem.
Hopefully this will be useful for future readers.
try this
b.SomeOtherProperty = "some new text";
dbCtx.BInstances.AddOrUpdate(b);
await (dbCtx.SaveChangesAsync(_token));
AddorUpdate will update your b instance if it is already added.
Given a database table with a column that contains historic data but that is no longer populated, is there a way in Entity Framework to read the column but prevent it being updated when using the same model object?
For example I have an object
public class MyObject
{
public string CurrentDataColumnName { get; set; }
public string HistoricDataColumnName { get; set; }
}
From the documentation I don’t believe I can do either of the following, because this will stop EF reading the data as well as persisting it.
(1) Decorate the HistoricDataColumnName property with the following attribute
[NotMapped]
(2) Add the following to my EntityTypeConfiguration for MyObject
Ignore(x => x.HistoricDataColumnName)
You can mark the column as computed to prevent Entity Framework from updating / inserting into that column.
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public string HistoricDataColumnName { get; set; }
DatabaseGenerated
An important database features is the ability to have computed
properties. If you're mapping your Code First classes to tables that
contain computed columns, you don't want Entity Framework to try to
update those columns. But you do want EF to return those values from
the database after you've inserted or updated data. You can use the
DatabaseGenerated annotation to flag those properties in your class
along with the Computed enum. Other enums are None and Identity.
You can simply use IsModified to check whether a specific entity property was modified or not and by this way you can still Read,Insert and Delete data:
var item = context.MyObjects.Find(id);
item.CurrentDataColumnName = "ChangedCurrentDataColumnName";
item.HistoricDataColumnName = "ChangedHistoricDataColumnName";
context.Entry(item).Property(c => c.HistoricDataColumnName).IsModified = false;
context.SaveChanges();
By using IsModified = false you are excluding the HistoricDataColumnName property from updating, so the HistoricDataColumnName column will not be updated in the database but other properties will be updated.
Setting this value to false for a modified property will revert the change by setting the current value to the original value. If the result is that no properties of the entity are marked as modified, then the entity will be marked as Unchanged. Setting this value to false for properties of Added, Unchanged, or Deleted entities is a no-op.
Check the following answer as a supplementary explanation. It might be helpful also:
https://stackoverflow.com/a/13503683/2946329
Codewise you can set the setter simply to protected. EF useses reflection to materialize your model. I think the now hidden setter also shows to every other programmer, that the field should not be modified any longer.
Also add an [Obsolete]-attribute with further information, why the property can't be set from the public anymore.
Since you say 'at the EF level or lower' a possible solution is to use a trigger to either raise an error if an attempt is made to change the column, or allow the update but ignore the change on the column of interest.
Option 1 - raise an error
CREATE TRIGGER MyTable_UpdateTriggerPreventChange
ON dbo.Table1
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
if update(HistoricDataColumnName)
begin
raiserror (50001, 16, 10)
end
END
Option 2 - ignore the change
CREATE TRIGGER MyTable_UpdateTriggerIgnore
ON dbo.Table1
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
update dbo.Table1 set HistoricDataColumnName=inserted.HistoricDataColumnName
from inserted
where inserted.Id = dbo.Table1.Id
END
You could of course do something similar for inserts if required.
Alternatively to raiserror use 'throw'
ALTER TRIGGER MyTable_UpdateTriggerPreventChange
ON dbo.Table1
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
if update(HistoricDataColumnName)
begin
throw 50002, 'You can''t change the historic data', 1
end
END
either way you'll get an exception thrown. This is using LinqPad
For just on column this is overkill, but in general you can override SaveChanges in DbContext to have more control on the changes.
In your model:
public override int SaveChanges()
{
var modifiedEntries = base.ChangeTracker.Entries<MyObject>()
.Where(e => e.State == EntityState.Modified).ToList();
foreach (var entry in modifiedEntries)
{
// Overwriting with the same value doesn't count as change.
entry.CurrentValues["HistoricDataColumnName"] = entry.OriginalValues["HistoricDataColumnName"];
}
return base.SaveChanges();
}
But you could also undo all modifications by changing the state from modified to unchanged.
-- UPDATE --
There is one thing that worries me. As soon as a developer has the credentials to access the database you cannot prevent them from doing things you don't want. They could create their own model or query the database directly.
So I think the most important thing to do is to set the field to readonly in the database for the client. But you may not be able to lock one column.
Even if this is not an issue, I think (for design) it is better to move all historical data to other tables. Making it easy to grant readonly access only. You can map these tables 1:1. With Entity Framework you can still access the historical information quite easy.
But in that case you won't have the problem you have now and will give you other options to prevent others from changing the historical information.
internal access modifier
You could change the setter to internal
public class MyObject
{
public string CurrentDataColumnName { get; internal set; }
public string HistoricDataColumnName { get; internal set; }
}
This doesn't impose as much limitations as the other options, but depending on your requirements, this can be quite useful.
protected access modifier
This would probably be the most common usage of making a property in EF "read-only". Which essentially only allows the constructor to access the setter (and other methods within the class, and classes derived from the class).
public class MyObject
{
public string CurrentDataColumnName { get; protected set; }
public string HistoricDataColumnName { get; protected set; }
}
I think protected is what you're looking for.
protected internal access modifier
You can also combine the two like this, to make it protected or internal
public class MyObject
{
public string CurrentDataColumnName { get; protected internal set; }
public string HistoricDataColumnName { get; protected internal set; }
}
Access Modifier Refresher Course
A internal member is accessible only within the same assembly
A protected member is accessible within its class and by derived class instances.
A protected internal member can be accessed from the current assembly or from types that are derived from the containing class.
The question is about EF 6, but this is easily doable in EF Core with the Metadata.IsStoreGeneratedAlways property. Thanks to ajcvickers on the EF Core repo for the answer.
modelBuilder
.Entity<Foo>()
.Property(e => e.Bar)
.ValueGeneratedOnAddOrUpdate()
.Metadata.IsStoreGeneratedAlways = true;
Why do this in EF in the first place? Why not simply ensure that any login being used to access the database either has the rights for performing UPDATE/INSERT/DELETE revoked or even go to the extreme of setting the database to READ_ONLY in the Database options?
It seems to me that any attempt to prevent updates via EF is doomed as you can always circumvent that and, for example, just execute SQL code directly against the EF connection.
As for me, it's simple solution - make property setters as private:
public class MyObject
{
public string CurrentDataColumnName { get; private set; }
public string HistoricDataColumnName { get; private set; }
}
EF will materialize objects from database without any problem, but yout won't have any way to change value int these properties.
I've made up a generic repository to make CRUD operations in a MVC project.
When i try to delete a row from a table that has an identity on SQLServer, the code generated by the Ormlite Delete method and inspected with the profiler doesn't not affect any rows.
This is the Crud operation for the deletion (pretty simple):
public void Destroy<T>(T entity)
{
using (var db = dbFactory.Open())
{
db.Delete<T>(entity);
}
}
The Type T in my test is represented by the following class:
[Alias("FT_TEST_DEVELOPMENT")]
public class TestTable
{
[AutoIncrement]
[PrimaryKey]
public int ID { get; set; }
public string DESCR { get; set; }
public DateTime? TIMESTAMP { get; set; }
public DateTime DATE { get; set; }
public decimal PRICE { get; set; }
public int? QTY { get; set; }
}
And the inspected code corresponds to the following:
exec sp_executesql N'DELETE FROM "FT_TEST_DEVELOPMENT" WHERE "ID"=#ID AND "DESCR"=#DESCR AND "TIMESTAMP"=#TIMESTAMP AND "DATE"=#DATE AND "PRICE"=#PRICE AND "QTY"=#QTY ',
N'#ID int,#DESCR nvarchar(6),#TIMESTAMP datetime,#DATE datetime,#PRICE decimal(2,0),#QTY int',
#ID=4,#DESCR=N'SECOND',#TIMESTAMP=NULL,#DATE='2015-06-01 00:00:00',#PRICE=15,#QTY=NULL
When I execute this perfectly sensed statement the server tells me that no row
Disclaimer: as some names where in my native language, I translated them so there may be little grammar error, if it's so, let me note and I'll edit.
UPDATE
The matching row actually EXISTS in the database
SELECT * FROM FT_TEST_DEVELOPMENT WHERE ID= 4
ID DESCR TIMESTAMP DATE PRICE QTY
4 SECOND NULL 2015-06-01 15 NULL
I mean that actually the OrmLite generated code appears to be bugged.
And yes, the ID column is the table's key.
SECOND UPDATE
I think I've found the cause:
actually in the WHERE clause the NULL fields are assigned in the way
#TIMESTAMP=NULL
but actually the SQL server will not match this statement, because it expects to receive
WHERE [...] AND "TIMESTAMP" IS NULL [...]
The way db.Delete() API works has been updated so that NULL fields are moved out of the parameterized queries and appended to the SQL filter so this should now work from v4.0.37+ that's now available on MyGet.
You can also delete rows in OrmLite by PrimaryKey with:
Db.DeleteById<TestTable>(entity.Id);
For generic methods you can use the T.GetId() extension method to get the value of the Id field, i.e:
Db.DeleteById<TestTable>(entity.GetId());
Or to delete using every non null property in the DELETE WHERE criteria, you can use:
Db.DeleteNonDefaults(entity);
If you execute the same statement in SSMS and nothing gets deleted, it's because no row matches the criteria.
OrmLite expects the primary key of an entity to be named Id (case-sensitive). Your property is named ID and the [PrimaryKey] attribute wasn't specified. In this case OrmLite has to use all available fields in the WHERE clause to find the rows to delete.
AutoIncrement doesn't mean the field is a key, just that its value is auto-generated by the server and comes from an identity column. The same applies with SQL Server - an identity column isn't a primary key, you need to define the primary key separately.
You need to either rename ID to Id or add the [PrimaryKey] attribute to it.