Deserializing readonly field

Posts   
 
    
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 09-Sep-2015 13:25:15   

Hello,

using LLBLGen 4.2 and SQLs erver, I have generated (adapter / DB first) the datalayer and using WebAPI/JSON to send the entities back & forth between frontend and backend.

Now I want to use optimistic concurrency, so I have added a "Timestamp" field to my tables in the DB and overridden the GetConcurrencyPredicate method in CommonEntityBase to facilitate this. It all works fine in a simple unittst, the ORMCocurrencyException gets thrown when appropriate.

The problem is when I want to involve the frontend. The entities that get serialized to JSON via WebAPI carry the Timestamp field, but after deserialization, the Timestamp field is always null. I first thought it was because the Timestamp is of type Byte[] and perhaps JSON had a problem with that, so I wrote a ByteArrayToInt64Converter, but it was still the same problem.

So now I suspect this is because the field "Timestamp" is indicated as readonly in the Designer, and hence has no setter method. I do not want to set the field mapping as not readonly in the designer, because that will probably generate subtle bugs later on, also then I have to remember to change the setting after each regeneration or when refreshing the DB to get new entities.

Is there some other approach to this problem ? Basically getting readonly fields to be deserialized correctly. It works for the Id field, and that one is also "readonly" in the designer, but it has a setter method ...

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 10-Sep-2015 07:16:18   

Do you have any simple code that reproduces this?

David Elizondo | LLBLGen Support Team
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 10-Sep-2015 09:44:54   

Sure, I've created a repro c#-solution in VS2013, see attach. It consists of a WebAPI project and a WinForms client project, and of course the LLBLGen adapter projects. The connection string in the webapi project points to a local database called "Sample" which has only one table :


CREATE TABLE [dbo].[Sample](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    Code [nvarchar](50) NOT NULL,
    [Description] [nvarchar](50) NULL,
    [Timestamp] [timestamp] NOT NULL,
 CONSTRAINT [PK_Sample] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

The WebAPI project has only 1 Controller, Sample controller which has 1 method, a HTTPGET to fetch the first SampleEntity record and return it :


 public class SampleController : ApiController
    {
        [HttpGet]
        public SampleEntity GetSample()
        {
            using (var adapter = new DataAccessAdapter())
            {
                var metaData = new LinqMetaData(adapter);
                var result = metaData.Sample.First();
                return result;
            }
        }
}

In the Winforms client, just a single form with a button and it's handler :


    private void button1_Click(object sender, EventArgs e)
        {
            string server = "http://localhost:1470/";
            string controller = "/api/sample";
            var client = new RestClient(server);
            var request = new RestRequest(controller, Method.GET);
            var response = client.Execute<SampleEntity>(request);
            var json = response.Content;
            SampleEntity sample = response.Data;
            var timestamp = sample.Timestamp;
        }

The solution is configured to run both the client as the server on startup. Also the NuGet packages (for WebAPI, LLBLGen, NewtonsoftJson and RestSharp) should be fetched automatically.

You can test the WebApi client by just browsing to

 http://localhost:1470/api/sample

, make sure you have added a record to the Sample table in the database wink In my case, I'm getting the JSON response :

{ $id: "1", Code: "test", Description: "a test record", Id: 1, Timestamp: "AAAAAAAAB9E=" }

If you run the client and put a breakpoint on the last line of code, you can inspect the variables. In the returned json, the timestamp is filled, but in the deserialized SampleEntity, the tiemstamp is empty.

I think this sample covers the core of my issue while keeping things simple.

Attachments
Filename File size Added on Approval
SampleConcurrency.zip 96,388 10-Sep-2015 09:45.09 Approved
Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 10-Sep-2015 20:06:44   

Reproduced.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 11-Sep-2015 09:23:47   

Our own serialization code (be it binary or xml) do support this, but I think any deserialization code which uses the property setters doesn't, and I think that's the problem here. I haven't tried your code yet, but I suspect if I manually add a setter to the property, deserialization works properly.

Frans Bouma | Lead developer LLBLGen Pro
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 11-Sep-2015 09:32:23   

Yup, i will today try a "workaround", by creating a new Timestamp2 property in the partial CommonEntityBase class, mark this one as DataContract so it serializes, and in the getter/setter propagate the value to the generated timestamp property, and in the LLBLGen designer add a rule to the DataMember attribute to ignore the "real" timestamp field so it doesn't popup in the json. That should work in my opinion, hopefully I got time this afternoon to try it out ...

But still it's a pity readonly properties cannet get sent over the wire via json ...maybe if a private or protected setter was implemented ?

BTW, i'm using the serialization as you explained on http://weblogs.asp.net/fbouma/how-to-make-asp-net-webapi-serialize-your-llblgen-pro-entities-to-jsonsimple_smile

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 11-Sep-2015 10:03:05   

You know what's so silly? When I add a setter to the property in the entity class, the response.Data field is null and no entity class is created on the client. No idea why. confused Doesn't matter whether it's a public or other setter, it's never called. (breakpoint on setter isn't hit, it is hit on setters of other properties).

I have no idea whether it's restsharp or something else, no exception is thrown either.

Frans Bouma | Lead developer LLBLGen Pro
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 11-Sep-2015 10:28:33   

That's indeed strange, because I don't have that behavior ...

I just tried to recreate in, in my sample project I have added a Unit test :


 [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestTimestamp()
        {
            string code = "testcode", description = "testdescription";
            byte[] timestamp = new byte[8] {0,1,2,3,4,5,6,7};
            SampleEntity entity = new SampleEntity();
            entity.Code = code;
            entity.Description = description;
            entity.Fields["Timestamp"].ForcedCurrentValueWrite(timestamp);

            string json = JsonConvert.SerializeObject(entity, LLBLgenJsonSerializerSettings.Settings);
            SampleEntity deserialized = JsonConvert.DeserializeObject<SampleEntity>(json, LLBLgenJsonSerializerSettings.Settings);

            Assert.IsTrue(entity.Code == deserialized.Code);
            Assert.IsTrue(entity.Description == deserialized.Description);
            Assert.IsTrue(entity.Timestamp == deserialized.Timestamp);


        }
    }

where the LLBLgenJsonSerializerSettings class is : (nuget install-package newtonsoft.json needed)


  public static class LLBLgenJsonSerializerSettings
    {
        public static JsonSerializerSettings Settings = new JsonSerializerSettings()
        {

            MissingMemberHandling = MissingMemberHandling.Ignore,
            NullValueHandling = NullValueHandling.Include,
            DefaultValueHandling = DefaultValueHandling.Include,
            //settings for LLBLGen
            PreserveReferencesHandling = PreserveReferencesHandling.Objects,
            ContractResolver = new DefaultContractResolver() { IgnoreSerializableInterface = true, IgnoreSerializableAttribute = true }
        };
    }

When I run the unit test, the json variable gets filled with the correct json : {"$id":"1","Code":"testcode","Description":"testdescription","Id":0,"Timestamp":"AAECAwQFBgc="} but the deserialized entity doesn't have the timestamp.

When, in the generated code SampleEntity.cs, I add a setter to the Timestamp property (linenumber 414):


        [DataMember]
        public virtual System.Byte[] Timestamp
        {
            get { return (System.Byte[])GetValue((int)SampleFieldIndex.Timestamp, true); }
            set { SetValue((int)SampleFieldIndex.Timestamp, value); } // <- line added !!

        }

and then re-run the unit test, the serialized json is still the same, but the Json.DeserializeObject<> method throws exception :


Test method SampleConcurrency.Tests.UnitTest1.TestTimestamp threw exception: 

Newtonsoft.Json.JsonSerializationException: Error setting value to 'Timestamp' on 'SampleConcurrency.EntityClasses.SampleEntity'. ---> SD.LLBLGen.Pro.ORMSupportClasses.ORMFieldIsReadonlyException: The field 'Timestamp' is read-only and can't be changed.
    at SD.LLBLGen.Pro.ORMSupportClasses.EntityCore`1.ValidateValue(IFieldInfo fieldToValidate, ref Object value, Int32 fieldIndex)
   at SD.LLBLGen.Pro.ORMSupportClasses.EntityCore`1.SetValue(Int32 fieldIndex, Object value, Boolean performDesyncForFKFields, Boolean checkForRefetch)
   at SD.LLBLGen.Pro.ORMSupportClasses.EntityCore`1.SetValue(Int32 fieldIndex, Object value)
   at SampleConcurrency.EntityClasses.SampleEntity.set_Timestamp(Byte[] value) in SampleEntity.cs: line 414
   at SetTimestamp(Object, Object)
   at Newtonsoft.Json.Serialization.DynamicValueProvider.SetValue(Object target, Object value)
 --- End of inner exception stack trace ---

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 11-Sep-2015 23:28:59   

Ah that exception will likely be the issue that I get null back I think, otherwise I don't know. Weird! I think the exception is swallowed or something, but then again it can't be it, as the breakpoint on the setter is never hit!

the exception you get is expected indeed. (you could work around that by using forcedCurrentValueWrite instead of setvalue()).

Anyway, we can't fix this now as the problem is that the setter is needed for deserialization and the readonly field doesn't have a setter. Adding the setter would still make the deserialization fail, so the setter should use a direct write but then that would be bad as that would work around the readonly-ness of the field.

disappointed

Frans Bouma | Lead developer LLBLGen Pro
HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 12-Sep-2015 15:20:39   

Hi,

I've implemented the workaround as I suggested in the OP and it works great !

So basically : - Have tables with SQL-timestamp columns (eg. called "Timestamp") - In the LLBL-designer, put a rule on the [DataMember]-attribute to exclude the timestamp field from the serialization (Name NotEqual Timestamp) - In CommonEntityBase partial class, put the [DataContract] attribute on the class, override the GetConcurrenyPredicate to use the Timestamp field for optimistic concurrency and add a new property "ActualTimestamp" with the [DataMember] attribute so that one will get serialized. In the get/set of ActualTimestamp, delegate to the Timestamp field (if it exists). The only overhead I see in this workaround is that every entity now will have the "ActualTimestamp" field in the generated json during serialization, even if it doesn't actually have a timestamp field in the DB. But I can live with that simple_smile

I had 2 unit tests, one directly on DB entities, and one on serialized/deserialized entities. Only the first one ran, now they both run simple_smile

For later reference in case someone runs into the same issues (I'm surprised nobody did yet) :

Code of CommonEntityBase (partial class):


  [DataContract]
    partial class CommonEntityBase
    {
        protected override IPredicateExpression GetConcurrencyPredicate(ConcurrencyPredicateType predicateTypeToCreate)
        {
            var field = Fields["Timestamp"];
            if (field == null)
                return null;//if no timestamp field was found, it's not possible to compare and do optimistic locking

            var expression = new PredicateExpression();
            switch (predicateTypeToCreate)
            {
                case ConcurrencyPredicateType.Save:
                    expression.Add((EntityField2)field == Fields["Timestamp"].DbValue);
                    break;
                case ConcurrencyPredicateType.Delete:
                    //don't do anything yet - should we be able to delete after another user changed the entity ?
                    break;
            }
            return expression;
        }


        [DataMember]
        public System.Byte[] ActualTimestamp
        {
            get
            {
                var field = Fields["Timestamp"];
                if (field == null)
                    return null;
                return field.CurrentValue as Byte[];
            }
            set
            {
                var field = Fields["Timestamp"];
                if (field != null)
                    field.ForcedCurrentValueWrite(value);
            }
        }
    }

My 2 now working unittests:


   [TestMethod]
        [ExpectedException(typeof(ORMConcurrencyException))]
        public void TestConcurrencyException_DirectOnDB()
        {
            IDataAccessAdapter adapter = new DataAccessAdapter(connectionstring);
            SampleEntity firstCopy = new SampleEntity(1);//for this to work, make sure a record with Id = 1 exists in DB !!
            SampleEntity secondCopy = new SampleEntity(1);
            adapter.FetchEntity(firstCopy);
            adapter.FetchEntity(secondCopy);
            firstCopy.Description = "unittestdata :" + DateTime.Now;
            secondCopy.Description = "otherunittestdata :" + DateTime.Now;
            adapter.SaveEntity(firstCopy);
            adapter.SaveEntity(secondCopy);//should throw the ORMConcurrencyException !!
        }


        [TestMethod]
        [ExpectedException(typeof(ORMConcurrencyException))]
        public void TestConcurrencyException_ViaSerialization()
        {
            IDataAccessAdapter adapter = new DataAccessAdapter(connectionstring);
            SampleEntity firstCopy = new SampleEntity(1);//for this to work, make sure a record with Id = 1 exists in DB !!
            SampleEntity secondCopy = new SampleEntity(1);
            adapter.FetchEntity(firstCopy);
            adapter.FetchEntity(secondCopy);
            firstCopy.Description = "unittestdata :" + DateTime.Now;
            secondCopy.Description = "otherunittestdata :" + DateTime.Now;
            var json1 = JsonConvert.SerializeObject(firstCopy, EntityJsonSerializer.Settings);
            var json2 = JsonConvert.SerializeObject(secondCopy, EntityJsonSerializer.Settings);
            var entity1 = JsonConvert.DeserializeObject<SampleEntity>(json1, EntityJsonSerializer.Settings);
            var entity2 = JsonConvert.DeserializeObject<SampleEntity>(json2, EntityJsonSerializer.Settings);
            //needed after deserialization
            entity1.IsNew = entity2.IsNew = false;
            adapter.SaveEntity(entity1);
            adapter.SaveEntity(entity2);//should throw the ORMConcurrencyException !!
        }

HcD avatar
HcD
User
Posts: 214
Joined: 12-May-2005
# Posted on: 13-Sep-2015 10:24:51   

ouch, I was a bit too optimistic rage

Although my unit test for the concurrency now run great, I cannot do a "regular save" anymore after sending an entity back and forth via json.

Using SQL profiler I saw that the update statements include WHERE [Timestamp] IS NULL instead of the real timestampvalue, which works fine when not going through serialization/deserialisation

I suspected it has to do with the "DbValue", and when debugging I've noticed indeed that during the setting of my "ActualTimestamp" property, when I call ForcecurrentValueWrite, the CurrentValue of the tiemstamp is set, but not the DBValue.

Is there anyway to also force the DBValue of this field ? ... I feel like I'm sooo close to the solution of this :-)

Edit : HA ! Got it to work simple_smile Stupid me, I was trying to see how to set the DbValue of the field, when it hit me ..only 10 lines of code above, I had set the ConcurrencyPredicateType.Save to :

  switch (predicateTypeToCreate)
            {
                case ConcurrencyPredicateType.Save:
                    expression.Add((EntityField2)field == (Fields["Timestamp"].DbValue));

I changed that line to use CurrentValue instead of DbValue, and now all the tests run - also the new ones I made to test regular saves simple_smile

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 14-Sep-2015 03:46:29   

HcD wrote:

Edit : HA ! Got it to work simple_smile Stupid me, I was trying to see how to set the DbValue of the field, when it hit me ..only 10 lines of code above, I had set the ConcurrencyPredicateType.Save to :

  switch (predicateTypeToCreate)
            {
                case ConcurrencyPredicateType.Save:
                    expression.Add((EntityField2)field == (Fields["Timestamp"].DbValue));

I changed that line to use CurrentValue instead of DbValue, and now all the tests run - also the new ones I made to test regular saves simple_smile

Good you figured it out sunglasses Thanks for the feedback.

David Elizondo | LLBLGen Support Team