dealing with dynamic properties on oData client - c#

I have the following class on both server and client
public class Entity
{
public string Id {get; set;}
public string Name {get; set;}
public Dictionary<string, object> DynamicProperties {get; set;}
}
As far as I have seen all the examples of open type describes about having dynamic properties on the server side, but the properties on the client needs to be explicitly declared.When I send a POST request from the client how do i send the dynamic properties ?. I can't declare all the dynamic properties on the client side. There are numerous properties and each object will contain different set of dynamic properties in the client side. These dynamic properties are stored in the DynamicProperties dictionary in the client side. How do I send the object of above entity class to the server side, so that server will interpret the contents of DynamicProperties dictionary as dynamic properties ?. Any help is appreciated.
===========================Follow-up for sam's answer=======================
static void Main(string[] args1)
{
container.Customers.ToList();
Customer newCustomer = new Customer();
newCustomer.Id = 19;
newCustomer.Properties = new Dictionary<string, object>
{
{"IntProp", 9},
{"DateTimeOffsetProp", new DateTimeOffset(2015, 7, 16, 1, 2, 3, 4, TimeSpan.Zero)},
{"blah","ha"}
};
try
{
addCustomer(newCustomer);
container.AddToCustomers(newCustomer);
container.SaveChanges();
}
catch (Exception)
{
}
Customer newCustomer1 = new Customer();
newCustomer1.Id = 20;
newCustomer1.Properties = new Dictionary<string, object>
{
{"IntProp", 10},
{"dir","north"}
};
addCustomer(newCustomer1);
container.AddToCustomers(newCustomer1);
container.SaveChanges();
newCustomer1.Properties["dir"] = "south";
container.UpdateObject(newCustomer1);
container.SaveChanges();
Console.ReadKey();
}
private static void addCustomer(Customer customer)
{
container.Configurations.RequestPipeline.OnEntryStarting(args =>
{
foreach (var property in customer.Properties)
{
args.Entry.AddProperties(new ODataProperty
{
Name = property.Key,
Value = property.Value // for enum, complex type, should to create ODataEnumValue and ODataComplexValue.
});
}
});
}
I am getting an error stating Multiple properties with the name 'IntProp' were detected in an entry or a complex value. In OData, duplicate property names are not allowed. Also, I doubt if creating an action each time before sending an object like how I am doing now is a valid approach as I get lot of objects from a source and I send it to the server. If I create an action for each object then it might blow up the memory as oData client holds these actions in memory. How do I handle my scenario ?. Kindly help me.
Also, one more question if I comment the container.Customers.ToList() it fails stating that I am trying to add undeclared properties. Why is that ?

If you are using OData Client Code Generator, you can use the partial class to define/retrieve/save the dyanmic properties.
For example, in your client side, you can define a partial class for your Entity
public partial class Entity
{
// Dynamic property "Email"
[global::Microsoft.OData.Client.OriginalNameAttribute("Email")]
public string Email
{
get
{
return this._Email;
}
set
{
this.OnEmailChanging(value);
this._Email = value;
this.OnEmailChanged();
this.OnPropertyChanged("Email");
}
}
private string _Email;
partial void OnEmailChanging(string value);
partial void OnEmailChanged();
}
Then, you can use this to insert/retrieve/save the dynamic property "Email".
You can do like this:
Container container = new Container(new Uri("http://..."));
Entity entity = new Entity();
...
entity.Email = "xxxx";
container.AddToEntities(entity);
container.SaveChanges();
For similar implementation, you can refer to my sample project.
========== iteration 2 ================
For client Entity class with IDictionary<string,object>, I think the hook is what you're looking for.
For example, on client side:
public partial class Entity
{
public IDictionary<string, object> Properties { get; set; }
.....
}
It should work if you insert the following codes before
container.AddToEntities(entity);
For example:
Entity entity = new Entity();
...
entity.Properties = new Dictionary<string, object>
{
{"IntProp", 9},
{"DateTimeOffsetProp", new DateTimeOffset(2015, 7, 16, 1, 2, 3, 4, TimeSpan.Zero)}
};
container.Configurations.RequestPipeline.OnEntryStarting(args =>
{
foreach (var property in entity.Properties)
{
args.Entry.AddProperties(new ODataProperty
{
Name = property.Key,
Value = property.Value
});
}
});
container.AddToEntities(entity);
container.SaveChanges();
Where, AddProperties is an extension method. You can find it in my sample project
and the latest commit
Besides, the hood method only works with OData Client V6.12 or above.
Hope it can help you.
========== iteration 3 ================
First, you call the following method,
container.Configurations.RequestPipeline.OnEntryStarting(...);
It means to add an action which will be called in later execution. In your codes, you call it twice, So, there are two actions added. These two actions will be called one by one when execute to save your newCustomer1
That's, newCustomer1 will have newCustomer's dynamic properties (action 1), meanwhile, it will have its own dynamic properties (action 2). That's why you got the duplicate property name exception.
To resolve it, you can just to renew a Container. See my project's update.
For container.Customers.ToList(), it seems an OData client issue.

[Answering my own question : Another approach]
Extending Sam Xu's approach for iteration 2. We can do it as below. (For the sake of clarity let's assume the name of the class in question as Book)
public partial class Book
{
public string ISBN {get; set;}
public IDictionary<string, object> DynamicProperties { get; set; }
}
// This portion can be put in a function and can be invoked only once
container.Configurations.RequestPipeline.OnEntryStarting(args =>
{
if(args.Entity.GetType() == typeof(Book))
{
var book = args.Entity as Book
foreach (var property in book.DynamicProperties)
{
args.Entry.AddProperties(new ODataProperty
{
Name = property.Key,
Value = property.Value
});
}
}
});
AddProperties extension method implementation is provided in Sam Xu's implementation

Related

Accessing fields filter parameters in IResourceService in JsonApiDotNetCore

I am currently trying to set up a Non-Entity Framework environment to access data via REST/JSON:API in ASP.NET Core 3.1 with https://github.com/json-api-dotnet/JsonApiDotNetCore
I followed the example as shown in: https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs
So here is my sample method:
public class DepartmentResourceService : IResourceService<Department>
{
public Task<IReadOnlyCollection<Department>> GetAsync(CancellationToken cancellationToken)
{
IReadOnlyCollection<Department> departments = new List<Department>{
new Department{ Id = 1, Name = "SE", Contact = "se#someaddress.at" },
new Department{ Id = 2, Name = "SD", Contact = "sd#someaddress.at" }
}.AsReadOnly();
return Task.FromResult(departments);
}
...
}
My example works pretty well, but I haven't figured out how I can access the given JSON:API fields-filter.
However: The filters do apply automatically somehow and only the given fields are sent which were defined in the query string, but the filter is applied after the object was generated.
When using EF, I can see that the sql-queries are already limited to the defined JSON:API fields-filter list, therefore the object is only filled up with informationen that was requested and nothing else.
I would like to do the same without EF, but I am missing that filter information in order to do so.
I could figure out, that there is an Interface called ITargetedFields (https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Resources/TargetedFields.cs/) and I thought maybe this could be injected into the constructor like so:
public class DepartmentResourceService : IResourceService<Department>
{
private readonly ITargetedFields targetedFields;
public DepartmentResourceService(ITargetedFields targetedFields)
{
this.targetedFields = targetedFields;
}
...
}
But the properties Attributes and Relationships Collections of ITargetedFields are always zero-length.
I couldn't find something in the docs or examples.
Any ideas?
I finally found a way, how to access query information, such as fields, pagination, includes in IResourceService<TResource>:
There are several IQueryConstraintProvider service providers registered in JsonApiApplicationBuilder in the method AddQueryStringLayer() with a Scope-Lifetime:
IIncludeQueryStringParameterReader
IFilterQueryStringParameterReader
ISortQueryStringParameterReader
ISparseFieldSetQueryStringParameterReader
IPaginationQueryStringParameterReader
IResourceDefinitionQueryableParameterReader
They are injected by DI in the constructor of your IResourceService<TResource> implementation, here is an example:
public class DepartmentResourceService : IResourceService<Department>
{
private readonly ISparseFieldSetQueryStringParameterReader _sparseFieldSetQueryStringParameterReader;
private readonly IIncludeQueryStringParameterReader _includeQueryStringParameterReader;
private readonly ISortQueryStringParameterReader _sortQueryStringParameterReader;
private readonly IPaginationQueryStringParameterReader _paginationQueryStringParameterReader;
public DepartmentResourceService(
ISparseFieldSetQueryStringParameterReader sparseFieldSetQueryStringParameterReader,
IIncludeQueryStringParameterReader includeQueryStringParameterReader,
ISortQueryStringParameterReader sortQueryStringParameterReader,
IPaginationQueryStringParameterReader paginationQueryStringParameterReader
)
{
_sparseFieldSetQueryStringParameterReader = sparseFieldSetQueryStringParameterReader;
_includeQueryStringParameterReader = includeQueryStringParameterReader;
_sortQueryStringParameterReader = sortQueryStringParameterReader;
_paginationQueryStringParameterReader = paginationQueryStringParameterReader;
}
public Task<IReadOnlyCollection<Department>> GetAsync(CancellationToken cancellationToken)
{
// Accessing all provided information:
IReadOnlyCollection<ExpressionInScope> constraints = _sparseFieldSetQueryStringParameterReader.GetConstraints();
IReadOnlyCollection<ExpressionInScope> includes = _includeQueryStringParameterReader.GetConstraints();
IReadOnlyCollection<ExpressionInScope> sortQuery = _sortQueryStringParameterReader.GetConstraints();
IReadOnlyCollection<ExpressionInScope> pagination = _paginationQueryStringParameterReader.GetConstraints();
// ***************************************************************
// Do what ever you need to do, with the information provided here
// ***************************************************************
// Return something
IReadOnlyCollection<Department> departments = new List<Department>{
new Department{ Id = 1, Name = "SE", Contact = "se#someaddress.at" },
new Department{ Id = 2, Name = "SD", Contact = "sd#someaddress.at" }
}.AsReadOnly();
return Task.FromResult(departments);
}
...
}

Dynamically Get and Set Properties on an EF Core Model

I'm learning C# and trying to find out how I can have a common method for updating different addresses that inherit from Address, and have an address type discriminator - PhysicalAddress and MailingAddress.
The example shows what I'd like to do if I could array access properties on models like you would in TypeScript. It indicates what I'm trying to do and have accomplished using two methods, but I haven't been able to figure out the way to achieve this in C#.
Any help, direction, or URL for referencing would be great. If this is not a good way to set up an update what would a properly single method update that would work with PerformUpdate.
// Example of an actual update method:
public async Task<int> PerformUpdate(User user, User updateUser) {
UpdateAddress(user, updateUser.PhysicalAddress);
UpdateAddress(user, updateUser.MailingAddress);
return await _context.SaveChangesAsync();
}
// Example of what I would like to achieve:
private void UpdateAddress(User user, Address newAddress)
{
// Currently would be: PhysicalAddress or MailingAddress
var addressType = newAddress.GetType().ToString();
// Dynamically access the address on the user based on the address type
Address oldAddress = user[addressType];
// Remove the old and add the new
if(oldAddress != null) {
_context.Addresses.Remove(oldAddress);
}
user[addressType] = newAddress;
}
I would look at the use case for this first, unless you have to support a truly huge number of classes it's probably better to create setters explicitly for each class you wish to support, for example with the is keyword. However, since you asked, we can abuse reflection to achieve this like so:
class Program
{
static void Main(string[] args)
{
var address = new PhysicalAddress();
var user = new User();
// this will set user.Address1 to address
SetAddress(user, address);
}
public static void SetAddress(User user, Address newAddress)
{
var fields = typeof(User).GetFields();
foreach (var fieldInfo in fields)
{
if (fieldInfo.FieldType == newAddress.GetType())
{
fieldInfo.SetValue(user, newAddress);
}
}
}
}
class User
{
public PhysicalAddress Address1;
public WorkAdress Address2;
}
class Address
{
}
class PhysicalAddress: Address
{
}
class WorkAdress: Address
{
}
```

DevForce Validation returning Ok for navigation properties that have not been set

I have been working on validatin for our entities in DevForce and I have managed to get everything I need working aside from validating Navigation Properties.
I have tried placing a RequiredValueVerifier attribute on the property and that does show the validation error on the UI but as soon as I use Manager.VerifierEngine.Execute({entitytovalidate}) the result comes back as Ok.
I know DevForce creates nullos and we can modify what the properties have in said nullos but I would like a way that the VeirifierEngine would return not Ok when we have not updated the value from the nullo.
My current work-around is to have a secondary Int32RangeVerifier on the Id that is used for the FKey but I am not to happy with that as a work-around.
Trying to do this without having to create Verifier Providers just for these properties.
If anyone has a solution to this I would be greatly appreciative if you could share.
Here is a sample of the current work-around:
namespace BearPaw.Models.Main
{
[MetadataType(typeof(TechnicianNoteMetadata))]
public partial class TechnicianNote {
public static TechnicianNote Create(int byUserId, DateTimeZone clientZone, DateTime userUtc)
{
var newItem = new TechnicianNote()
{
CreatedById = byUserId,
CreatedDate = userUtc,
CreatedDateTz = clientZone.Id,
ModifiedById = byUserId,
ModifiedDate = userUtc,
ModifiedDateTz = clientZone.Id
};
return newItem;
}
}
public class TechnicianNoteMetadata
{
[Int32RangeVerifier(ErrorMessage = "Note Category is required", MinValue = 1)]
public static int NoteCategoryId;
[RequiredValueVerifier(DisplayName = "Note Category")]
public static NoteCategory NoteCategory;
[RequiredValueVerifier(DisplayName = "Note Detail")]
public static string NoteDetail;
}
}
Many thanks in advance
You can create a custom verifier to handle navigation property validation, and add it directly to the VerifierEngine with the AddVerifier method if you don't want to use an IVerifierProvider.
For example:
public class NullEntityVerifier : PropertyValueVerifier
{
public NullEntityVerifier(
Type entityType,
string propertyName,
string displayName = null)
: base(new PropertyValueVerifierArgs(entityType, propertyName, true, displayName)) { }
public NullEntityVerifier(PropertyValueVerifierArgs verifierArgs)
: base(verifierArgs) { }
protected override VerifierResult VerifyValue(object itemToVerify, object valueToVerify, TriggerContext triggerContext, VerifierContext verifierContext)
{
var entity = valueToVerify as Entity;
var msg = $"{this.ApplicableType.Name}.{this.DisplayName} is required.";
return new VerifierResult(entity != null && !entity.EntityAspect.IsNullEntity, msg);
}
}
To add to the engine:
var verifier = new NullEntityVerifier(typeof(TechnicianNote), "NoteCategory");
_em1.VerifierEngine.AddVerifier(verifier);
If you want to stick with attributed verifiers you can create a custom attribute for your verifier. See the DevForce Resource Center for more information.

Provide documentation for ASP.NET Web Api method with a dynamic query string

I'm using Help Pages for ASP.NET Web API to create documentation for our web api. Everything is working fine using the XML documentation comments. However, for one method I can't figure out how to supply documentation for a dynamic query string.
The method uses the GetQueryNameValuePairs() of the request to select the key-value pairs of the query string to a model. For example ?1=foo&2=bar will result in a list of two objects with Id set to 1 and 2 and Value to 'foo' and 'bar', respectively.
I've tried adding the <param> tag to the XML comment, but this is ignored since the method does not contain a matching parameter.
Any help would be appreciated.
You could try extending the help page generation process. When you create your ASP.NET Web API project, the help page-related code is downloaded as source, not as a .dll, so you can extend it with any custom logic you'd like.
Here's what I would do:
Create an attribute class and decorate my special method with that (e.g. [DynamicQueryParameter("Param1",typeof(string))])
Modify the HelPageConfigurationExtensions.cs to query these attributes from the actions as well and add them manually to the UriParameters collection of the model. I would probably do this in the GenerateUriParameters() method.
[Edit] I actually had some time, so I put together the solution myself, because, you know, it's fun :)
So create an an attribute:
public class DynamicUriParameterAttribute : Attribute
{
public string Name { get; set; }
public Type Type { get; set; }
public string Description { get; set; }
}
You can decorate your action methods with this:
[DynamicUriParameter(Description = "Some description", Name ="Some name", Type =typeof(string))]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
Then I modified the HelpPageConfigurationExtensions.GenerateApiModel() like this:
private static HelpPageApiModel GenerateApiModel(ApiDescription apiDescription, HttpConfiguration config)
{
HelpPageApiModel apiModel = new HelpPageApiModel()
{
ApiDescription = apiDescription,
};
ModelDescriptionGenerator modelGenerator = config.GetModelDescriptionGenerator();
HelpPageSampleGenerator sampleGenerator = config.GetHelpPageSampleGenerator();
GenerateUriParameters(apiModel, modelGenerator);
// add this part
var attrs = apiDescription.ActionDescriptor.GetCustomAttributes<DynamicUriParameterAttribute>();
foreach (var attr in attrs)
{
apiModel.UriParameters.Add(
new ParameterDescription
{
Name = attr.Name,
Documentation = attr.Description,
TypeDescription = modelGenerator.GetOrCreateModelDescription(attr.Type)
}
);
}
// until here
GenerateRequestModelDescription(apiModel, modelGenerator, sampleGenerator);
GenerateResourceDescription(apiModel, modelGenerator);
GenerateSamples(apiModel, sampleGenerator);
return apiModel;
}

Prevent AutoMapper to treat string as collection in LINQ ProjectTo

I've the following set of classes to map (only in one direction, from Data* to Api*):
// Top level
public class DataEntity
{
public NestedDataEntity Nested { get; set; }
// ... other primitive/complex properties
}
public class ApiEntity
{
public NestedApiEntity Nested { get; set; }
// ... other primitive/complex properties
}
// Nested level
public class NestedDataEntity
{
public string Items { get; set; }
}
public class NestedApiEntity
{
public IEnumerable<ApiSubItem> Items { get; set; }
}
public class ApiSubItem
{
// there are properties here. Not needed for the sake of example
}
Mapping is configured within a Profile as in the following bit of code:
// mapping profile
public class MyCustomProfile : Profile
{
public MyCustomProfile()
{
CreateMap<DataEntity, ApiEntity>();
CreateMap<NestedDataEntity, NestedApiEntity>();
CreateMap<string, IEnumerable<ApiSubItem>>()
.ConvertUsing<TextToSubItemsConverter>();
}
}
// type converter definition
public class TextToSubItemsConverter :
ITypeConverter<string, IEnumerable<ApiSubItem>>
{
public IEnumerable<ApiSubItem> Convert(
string dataItems, IEnumerable<ApiSubItem> apiItems, ResolutionContext context)
{
// actually, deserialize & return an ApiSubItem[]
// here just return some fixed array
return new ApiSubItem[]
{
new ApiSubItem(),
new ApiSubItem(),
new ApiSubItem(),
};
}
}
// Main
public class Program
{
public static void Main(string[] args)
{
Mapper.Initialize(cfg =>
{
cfg.AddProfile<MyCustomProfile>();
});
Mapper.AssertConfigurationIsValid();
DataEntity dataEntity = new DataEntity()
{
Nested = new NestedDataEntity()
{
Items = "Ignored text",
},
};
// This maps ok, no issues
ApiEntity apiEntity = Mapper.Map<DataEntity, ApiEntity>(dataEntity);
IQueryable<DataEntity> dataEntities = new[] { dataEntity }.AsQueryable();
// This exposes the System.Char to ApiSubItem issue
ApiEntity apiEntityProjected = dataEntities.ProjectTo<ApiEntity>().First();
}
}
Mapping configuration passes initial validation at startup, but then when an actual mapping is required I get exception:
System.InvalidOperationException: Missing map from System.Char to my.whatever.namespace.ApiSubItem. Create using Mapper.CreateMap.
If I omit completely the configuration between string and IEnumerable<ApiSubItem>, initial validation complains about the same context:
Context:
Mapping from type System.Char to my.whatever.namespace.ApiSubItem
although when added, it seems like it's not picked up by AutoMapper.
The mapping happens in a static context, through a ProjectTo<ApiEntity>() call on top of LINQ query over DbSet<DataEntity>. I've checked that converter does not require any dependency, just in case.
AutoMapper is 5.0.2, running in an MVC API web application under ASP.Net Core 1 RTM.
I've checked similar issues here on SO, but with no luck. Does anyone know how to make this type of scenario work? I guess I'm not the first one trying to (Auto)map from a string to a collection. TA
EDIT Added both failing and non-failing case to example
EDIT 2 After further search, somewhere someone suggested to ignore the field and perform mapping in AfterMap(). With this profile, exception is not thrown but resulting Items field is null:
CreateMap<DataEntity, ApiEntity>();
CreateMap<NestedDataEntity, NestedApiEntity>()
.ForMember(api => api.Items, options => options.Ignore())
.AfterMap((data, api) =>
{
api.Items = Mapper.Map<IEnumerable<ApiSubItem>>(data.Items);
});
CreateMap<string, IEnumerable<ApiSubItem>>()
.ConvertUsing<TextToSubItemsConverter>();
EDIT 3 Reworded question title to make it more specific for projection
I guess the actual answer is along the lines of "projections and converter/resolvers don't play nicely together". From AutoMapper Queryable.Extensions wiki:
Not all mapping options can be supported, as the expression generated must be interpreted by a LINQ provider. [...]
Not supported:
[...]
Before/AfterMap,
Custom resolvers,
Custom type converters
EDIT Workaround:
In this particular scenario, I've later found to be useful having an intermediate DTO entity that could grab some more fields, not needed for the final API entity but still needed for business logic.
In such DTO entity I left the string field as in DataEntity, so I could use projection and let AutoMapper/LinqToSQL grab only needed fields from DB.
Then, between DTO entity and API entity already in memory, I could apply a second mapping which also took advantage of custom converter for string ==> IEnumerable<ApiSubItem> mapping. HTH

Categories