What is the correct way to tell WebApi2 and EF6 to not serialize a field to Json? - entity-framework

I have an Entity Framework Table per Type hierarchy like this:
public class WorkItem
{
public int WorkItemId {get;set;}
}
public class CancelingWorkItem : WorkItem
{
public int WorkItemIdToCancel {get;set;}
[ForeignKey("WorkItemIdToCancel")]
public virtual WorkItem WorkItemToCancel {get;set}
}
public class SomeOtherWorkItem : WorkItem
{
// more fields...
}
When I return a list of all WorkItems in the database as Json, any serialized CancelingWorkItem will contain the full definition of the WorkItemToCancel field. I could just ignore this field with JsonIgnore, but I was wondering if there was a different/better way of doing this. My repository project doesn't yet rely on Json.Net, so if I can instead tell the controller not to serialize that field, that might be a better solution.

You can use the IgnoreDataMemberAttribute attribute - It's not from an external library like JsonIgnore and I think that the default serializer and Json.NET will both recognise this attribute.

Related

OData select with complex data type

I want to retrieve a single property from a complex data type.
Platform: EF Core 6, OData V4, Blazor on Windows 11, VS 2022 on a MS SQL Express database.
Simplified DB / entity structure:
[Owned]
public class FileInfo
{
[StringLength(255)]
public string Filename { get; set };
}
public class UserInfo
{
[StringLength(80)]
public string UserID { get; set; }
[StringLength(200)]
public string Name { get; set; }
[StringLength(200)]
public string Email { get; set; }
}
public class Document
{
[Key]
public Guid DocumentID { get; set; }
public FileInfo FileInfo { get; set; }
[StringLength(80)]
public string OwnerID { get; set; }
public virtual UserInfo? Owner { get; set; }
}
public class Request
{
[Key]
public Guid RequestID { get; set; }
[StringLength(80)]
public string AuthorID { get; set; }
[ForeignKey("AuthorID")]
public virtual UserInfo? Author;
public Guid DocumentID { get; set; }
[ForeignKey("DocumentID")]
public virtual Document? Document;
}
Entities etc.:
public static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Document>("Documents");
modelBuilder.EntitySet<Request>("Requests");
modelBuilder.ComplexType<FileInfo>();
return modelBuilder.GetEdmModel();
}
Query #1:
https://localhost:12345/TestApp/Requests?
$count=true&
$select=Document&
$orderby=Document/FileInfo/Filename&
$expand=Document($select=FileInfo/Filename)
This query returns:
{"#odata.context":"https://localhost:44393/DocServer2/
$metadata#Requests(Document,Document(FileInfo/Filename))",
"# odata.count":3,"value":[
{"Document":{"FileInfo":{"Filename":"BST-abc-dd100-04.pdf"}}},
{"Document":{"FileInfo":{"Filename":"BST-abc-dd100-04.pdf"}}},
{"Document":{"FileInfo":{"Filename":"BST-DEF-DD100-01.PDF"}}}]}
However, I actually only need a list of strings (the property values).
This is not all though. Things are getting ugly when I apply a filter to the query, requiring me to look at and hence expand more data:
https://localhost:12345/TestApp/Requests?
$count=true&$orderby=Document/FileInfo/Filename&
$select=Document&
$filter=(Document/OwnerID eq 'Testuser') or (AuthorID eq 'TestUser')&
$expand=Author,Document($expand=Owner;$select=FileInfo/Filename)
The result looks like this:
{"#odata.context":"https://localhost:12345/TestApp/
$metadata#Requests(
Document,Author(),
Document(FileInfo/Filename,
Owner()))",
"#odata.count":1,
"value":[
{"Author":{"Name":"Test User"},
"Document":{"FileInfo":{"Filename":"Test.PDF"},
"Owner":{"Name":"Test User"}}}]}
Note: Using "$select=" instead of "$select=Document" returns all property values of Document (seems to be treated like "select * from Documents").
How do I need to adjust the query to only return Request.Document.FileInfo.Filename?
I did google and also searched SO for an answer, but couldn't find one.
Update: Thankyou for updating the post with the platform/vendor version, that changes everything, you are not asking about standards anymore but about a specific implementation, which is the correct approach.
You are correct that to $select a specific property on a ComplexType you should use the / to address it as a descendant of the name of the root property:
https://localhost:12345/TestApp/Requests?
$count=true&
$select=Document&
$orderby=Document/FileInfo/Filename&
$expand=Document($select=FileInfo/Filename)
NOTE: Some server-side implementations or constraints might require that certain fields are returned, even if you do not request them. This is a server-side configuration and is outside of the scope of the specifications. The specs do not specifically state that non-requested fields cannot be returned, only that the requested fields MUST be included in the response. Custom implementations are allowed to return additional properties as long as they are declared correctly in the $metadata then they will be supported.
Unfortunately for your case a key tenant of OData V4 over other APIs is that the structure of the resource will not change. Entities are resources (the R in REST) and OOTB in the .Net implementations this cannot be violated. This means that the response will always be an array of Request objects that have a single Document property that also has a single FileInfo that has a single Filename property.
So from a pure OData v4 specification point of view, what you are asking for is totally against the core principle of OData (v4), read on for additional variations and exceptions to this rule...
RE: This is not all though. Things are getting ugly when I apply a filter to the query, requiring me to look at and hence expand more data:
There is no reason that you need to include other $expand or $select properties to evaluate a $filter. $filter is evaluated first and independently from (and before) the $select and $expand. So you are not required to include these properties in your request at all, but if you do include those navigation pathways in the request, it makes sense that those fields and/or navigation properties would be included in the response.
If you query the Requests controller, then according to the OData specification, the response should be in the shape of a Response object. We can used $select and $expand to reduce the bytes transferred over the wire by omitting properties, but relationship structure or general shape of the object graph MUST be maintained to allow the client-side implementations to work correctly.
{
"#odata.context": "https://localhost:44393/DocServer2/$metadata#Requests(Document,Document(FileInfo/Filename))",
"# odata.count": 3,
"value": [
{
"Document": {
"FileInfo": {
"Filename": "BST-abc-dd100-04.pdf"
}
}
},
{
"Document": {
"FileInfo": {
"Filename": "BST-abc-dd100-04.pdf"
}
}
},
{
"Document": {
"FileInfo": {
"Filename": "BST-DEF-DD100-01.PDF"
}
}
}
]
}
If you are expecting a simple OData array of strings like the following, then you will have to write some extra code:
{
"value": [
"BST-abc-dd100-04.pdf",
"BST-abc-dd100-04.pdf",
"BST-DEF-DD100-01.PDF"
]
}
or perhaps, if you want a pure custom REST/JSON response you can do that too, but it's not conformant to the OData specification anymore:
[
"BST-abc-dd100-04.pdf",
"BST-abc-dd100-04.pdf",
"BST-DEF-DD100-01.PDF"
]
Previous versions of OData did support direct querying of child resources, but in v4 specification this is only supported by Entity navigation links
You OData controllers are just a great start for whatever you want to add to your OData implementation. If you have a genuine need to return a flattened list, then you can add an additional function to your controller to support this,
There is a feature described in the OData 4.01 amended specification that does allow you to use an alias to reference the result of a $compute query option. However, this was not included in the specification until 2020, not many older implementations are likely to have support for this new option, EF Core (Microsoft.AspNetCore.OData v8.0.12) only has partial support for this syntax.
It is expected to work like this:
https://localhost:12345/TestApp/Requests?
$count=true&
$select=File&
$orderby=File&
$compute=Document/FileInfo/Filename as File
Should result in something similar to this:
{"#odata.context":"...",
"#odata.count":3,"value":
[{"File":"test1.pdf"},
{"File":"test2.pdf"},
{"File":"test3.PDF"}
]}
Unfortunately as I test this I encounter a bug in Microsoft.AspNetCore.OData v8.0.12 that does not allow you to $select the aliased column, you can see the column included if you use $select=* but I cannot scope the response to just that column.
Please try it on your API to confirm, but until $compute works if you have need of a specific shape of data, then you should add a function or action endpoint to return that desired data. OData is just a tool to help you get there, using OData does not preclude you from adding custom endpoints, as long as you define them correctly, they will still be exposed through the metadata and can be easily consumed by clients that implement code generators.
To implement a custom function to retrieve this data, you can use a controller method similar to this:
[HttpGet]
[EnableQuery]
public async Task<IActionResult> Filenames()
{
IQueryable<Request> query = GetRequestsQuery();
return Ok(query.Select(x => x.Document.FileInfo.Filename).ToArray());
}
...
builder.EntitySet<Request>("Requests").EntityType.Collection.Function(nameof(Filenames)).ReturnsCollection<string>();
Then you could query this via the following URL:
https://localhost:12345/TestApp/Requests/Filenames
However OData Query Options can only be enforced on the response type of the method, so even with [EnableQuery] OOTB you can only $filter or $orderby the values in the Filename property.
There are other workarounds, including Open Type support, but if you are interested in the $compute solution but cannot get it to work, then we should raise an issue with https://github.com/OData/odata.net/issues?q=compute to get the wider community involved.

Using REST to try and get Field Service Detail - InventoryID = SQL error: Multi-part identifier not found

I am getting SQL errors when trying to use REST to get to FSAppointmentDet.InventoryID, either as a Field Service service item or as an Inventory Item.
The InventoryID field exists in the table, however, it looks like the DACs have been inherited, for example as FSAppointmentDetService.
Other fields work, it just seems that the fields with an ID are causing the SQL error.
In this case, the SQL error is a multi-step identifier not found. Running a SQL Profiler trace and looking at the SQL, it looks like the table has been aliased in one part of the query and not in another. Obviously this is occurring at a level much lower than we can get to, so looking for a workaround or ideas on how to get the InventoryID for Field Service detail records.
I've seen this happen when one DAC herits (herits as in class inheritance not extend as in DAC extension) from another DAC without redeclaring it's key fields. The way to fix that is to add the parent keys abstract class fields in the children.
FSAppointmentDetService seems to be missing AppointmentID key declaration. When the ORM builds the SQL query it generates Alias for the herited DAC but it gets confused becaused the key fields of the parent were not all re-declared in the child.
In FSAppointmentDet you have 2 key fields:
#region AppointmentID
public abstract class appointmentID : PX.Data.IBqlField
{
}
[PXDBInt(IsKey = true)]
[PXParent(typeof(Select<FSAppointment, Where<FSAppointment.appointmentID, Equal<Current<FSAppointmentDet.appointmentID>>>>))]
[PXDBLiteDefault(typeof(FSAppointment.appointmentID))]
[PXUIField(DisplayName = "Appointment Nbr.")]
public virtual int? AppointmentID { get; set; }
#endregion
#region AppDetID
public abstract class appDetID : PX.Data.IBqlField
{
}
[PXDBIdentity(IsKey = true)]
public virtual int? AppDetID { get; set; }
#endregion
But in FSAppointmentDetService only one of them is redeclared. Notice how it's using 'override' to redeclare compared to FSAppointmentDet which do not override:
#region AppDetID
public new abstract class appDetID : PX.Data.IBqlField
{
}
[PXDBIdentity(IsKey = true)]
public override int? AppDetID { get; set; }
#endregion
In this case we can't add field to that DAC though because it's part of the base product. I think it would be possible to create a new DAC that herits from FSAppointmentDetService, add the missing key in there and use that new herited DAC instead of FSAppointmentDetService.
However I don't know if that would be possible when working with Web Services. If not the change will have to be made in Acumatica base product. You could fill a bug report with Acumatica support to have that done in future versions.

Summary column on EF

Is it possible to add summary properties(no database column) according LINQ from another property(column) in EF generated class from database and this property don't update(delete or remove from class) when update model from database(because this property(cloumn) is not on database)
Yes, it is. Classed generated by Entity Framework as an Entitied are always marked partial. It lets you extend the functionality with your own properties or method.
Let say your entity class is named Post. You can extend it with code like that:
public partial class Post
{
public int Average
{
get
{
return this.Items.Average();
}
}
}
Because it's not a part of designer-generated file it won't be overwritten when it's regenerated. However, there is one requirement to make it work: your custom part of Post class has to be in exactly the same namespace as code generated by EF.
Try using the [NotMapped] attribute on a property in a partial class. This will be ignored by Entity Framework.
public partial class EntityName
{
[NotMapped]
public int CalculatedProperty
{
get
{
return Numbers.Sum();
}
}
}

Serialization in ASP.NET Web API

I am using XmlSerializer instead of DataContractSerializer in my ASP.NET Web API project and have a return object defined as
Response Object
public class MyResponse
{
public string Name {get;set;}
public CustomField<string> Username {get;set;}
public CustomField<float?> Score {get;set;}
}
Custom Field
public class CustomField<T>
{
public T Value {get;set;}
public long LastModified {get;set;}
}
I want to generate an XML response as
<MyResponse>
<FirstName>ABC</FirstName>
<Username lastModified="1234">XYZ</Username>
<Score lastModified="45678">12002</Score>
</MyResponse>
ASP.NET Web API returns a JSON object (I am aware that this happens when XmlSerialization does not work correctly) when I decorate the CustomField class as
public class CustomField<T>
{
[XmlText]
public T Value {get;set;}
[XmlAttribute]
public long LastModified {get;set;}
}
How can I get the desired XML response ?
Alright, I think I know what's going on.
If you try to run
new XmlSerializer(typeof(MyResponse))
you'll get this error:
System.InvalidOperationException: Cannot serialize member 'Value' of type System.Nullable`1[System.Single]. XmlAttribute/XmlText cannot be used to encode complex types.
So the issue is that you have a field of type 'float?' as an [XmlText]. [XmlText] can only be applied to primitives, and it doesn't look like XmlSerializer recognizes 'float?' as a primitive. If you use 'float' instead of 'float?', everything looks to be working right. If you want to indicate that sometimes there is no Score, you may want to set the Score to null instead of the Score's value to null.
Hope that helps.

Entity Framework and implementation of IPrincipal/IIdentity

As far as I am aware, for the property to be saved in the database it cannot be ReadOnly.
IIdentity properties: AuthenticationType, IsAuthenticated and Name are all ReadOnly.
Is making the wrapper to the properties that need to be saved the only solution or there are better ones?
EDIT:
I might not have explained my question that well. Here is the sample code for one of the ReadOnly properties, I have added UserName property for the Entity Framework:
Public Property UserName As String
Get
Return _userName
End Get
Private Set(value As String)
userName = value
End Set
Public ReadOnly Property Name As String Implements System.Security.Principal.IIdentity.Name
Get
Return UserName
End Get
End Property
What I wanted to ask is if there is any better way of doing it.
IIdentity properties are read only but the implementation can have setters. If you are using EDMX for mapping you don't have to expose these setters as public.
Edit:
This is possible in C# so hopefully you can use similar approach with VB.NET (I can only read VB code, not write):
public interface ITest {
string Name { get; }
}
public class Test : ITest {
public string Name { get; set; }
}
The class offers setter even the interface didn't define it.
The EF persists objects, not interfaces. Your object can have whatever properties you would like it to have. You cannot add an interface to your entity model, but you can add an object type which implements that interface.