I am using HttpPatch to partially update an object. To get that working I am using Delta and Patch method from OData (mentioned here: What's the currently recommended way of performing partial updates with Web API?). Everything seems to be working fine but noticed that mapper is case sensitive; when the following object is passed the properties are getting updated values:
{
"Title" : "New title goes here",
"ShortDescription" : "New text goes here"
}
But when I pass the same object with lower or camel-case properties, Patch doesn't work - new value is not going through, so it looks like there is a problem with deserialisation and properties mapping, ie: "shortDescription" to "ShortDescription".
Is there a config section that will ignore case sensitivity using Patch?
FYI:
On output I have camel-case properties (following REST best practices) using the following formatter:
//formatting
JsonSerializerSettings jss = new JsonSerializerSettings();
jss.ContractResolver = new CamelCasePropertyNamesContractResolver();
config.Formatters.JsonFormatter.SerializerSettings = jss;
//sample output
{
"title" : "First",
"shortDescription" : "First post!"
}
My model classes however are follwing C#/.NET formatting conventions:
public class Entry {
public string Title { get; set;}
public string ShortDescription { get; set;}
//rest of the code omitted
}
Short answer, No there is no config option to undo the case sensitiveness (as far as i know)
Long answer: I had the same problem as you today, and this is how i worked around it.
I found it incredibly annoying that it had to be case sensitive, thus i decided to do away with the whole oData part, since it is a huge library that we are abusing....
An example of this implementation can be found at my github github
I decided to implement my own patch method, since that is the muscle that we are actually lacking. I created the following abstract class:
public abstract class MyModel
{
public void Patch(Object u)
{
var props = from p in this.GetType().GetProperties()
let attr = p.GetCustomAttribute(typeof(NotPatchableAttribute))
where attr == null
select p;
foreach (var prop in props)
{
var val = prop.GetValue(this, null);
if (val != null)
prop.SetValue(u, val);
}
}
}
Then i make all my model classes inherit from *MyModel*. note the line where i use *let*, i will excplain that later. So now you can remove the Delta from you controller action, and just make it Entry again, as with the put method. e.g.
public IHttpActionResult PatchUser(int id, Entry newEntry)
You can still use the patch method the way you used to:
var entry = dbContext.Entries.SingleOrDefault(p => p.ID == id);
newEntry.Patch(entry);
dbContext.SaveChanges();
Now, let's get back to the line
let attr = p.GetCustomAttribute(typeof(NotPatchableAttribute))
I found it a security risk that just any property would be able to be updated with a patch request. For example, you might now want the an ID to be changeble by the patch. I created a custom attribute to decorate my properties with. the NotPatchable attribute:
public class NotPatchableAttribute : Attribute {}
You can use it just like any other attribute:
public class User : MyModel
{
[NotPatchable]
public int ID { get; set; }
[NotPatchable]
public bool Deleted { get; set; }
public string FirstName { get; set; }
}
This in this call the Deleted and ID properties cannot be changed though the patch method.
I hope this solve it for you as well. Do not hesitate to leave a comment if you have any questions.
I added a screenshot of me inspecting the props in a new mvc 5 project. As you can see the Result view is populated with the Title and ShortDescription.
It can be done quite easily with a custom contract resolver that inherits CamelCasePropertyNamesContractResolver and implementing CreateContract method that look at concrete type for delta and gets the actual property name instead of using the one that comes from json. Abstract is below:
public class DeltaContractResolver : CamelCasePropertyNamesContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
// This class special cases the JsonContract for just the Delta<T> class. All other types should function
// as usual.
if (objectType.IsGenericType &&
objectType.GetGenericTypeDefinition() == typeof(Delta<>) &&
objectType.GetGenericArguments().Length == 1)
{
var contract = CreateDynamicContract(objectType);
contract.Properties.Clear();
var underlyingContract = CreateObjectContract(objectType.GetGenericArguments()[0]);
var underlyingProperties =
underlyingContract.CreatedType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in underlyingContract.Properties)
{
property.DeclaringType = objectType;
property.ValueProvider = new DynamicObjectValueProvider()
{
PropertyName = this.ResolveName(underlyingProperties, property.PropertyName),
};
contract.Properties.Add(property);
}
return contract;
}
return base.CreateContract(objectType);
}
private string ResolveName(PropertyInfo[] properties, string propertyName)
{
var prop = properties.SingleOrDefault(p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
if (prop != null)
{
return prop.Name;
}
return propertyName;
}
}
Related
There is a sample here for creating 100% dynamic OData models in Microsoft.AspNetCore.OData 8.x. However, in our case we have an existing model that we are happy with, but we want to add custom fields to it.
In other words, we want an OData model with entities that have some fixed columns/properties and some dynamically-generated columns/properties that come from the database, like this:
public class ODataEntity
{
[Key]
public int Id { get; set; }
public string Name { get; set; } = "";
// From the perspective of clients like Power BI, this should produce
// a series of additional columns (the columns are the same on all
// instances, but the schema can change at any time)
public Dictionary<string, object> CustomFields { get; set; }
}
To my tremendous surprise, the key-value pairs in CustomFields become properties in the JSON output (i.e. there is no CustomFields column; its contents are inserted into the parent object). However, the custom fields are not recognized by Power BI:
I assume that this is because there is no metadata for the custom fields in https://.../odata/$metadata. So my question is:
How can I modify the following code so that the custom columns are included in the IEdmModel?
static IEdmModel GetEdmModel(params CustomFieldDef[] customFields)
{
var builder = new ODataConventionModelBuilder() {
Namespace = "Namespace",
ContainerName = "Container", // no idea what this is for
};
builder.EntitySet<ODataEntity>("objects");
return builder.GetEdmModel();
}
public class CustomFieldDef
{
public string FieldName;
public Type Type;
}
How can I modify the following startup code so that the IEdmModel is regenerated every time https://.../odata/$metadata is accessed?
IMvcBuilder mvc = builder.Services.AddControllers();
mvc.AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel())
.Select().Filter().OrderBy().Count().Expand().SkipToken());
Regarding the first question, there are two basic approaches that one could take:
1. Dynamic Everything
Use the 100% dynamic approach that is used in the ODataDynamicModel sample. This approach is difficult, especially if you already have a working model, because (i) the way the sample code works is difficult to understand, and (ii) you have to completely rewrite your code, or use reflection to help generate both the schema ($metadata) and the output data.
2. Modify the EdmModel
This is the approach I'm taking in this answer.
Step 1: change the model
The IEdmModel returned by ODataConventionModelBuilder.GetEdmModel is mutable (and actually has type EdmModel), so you can add custom fields to it.
static IEdmModel GetEdmModel(params CustomFieldDef[] customFields)
{
var builder = new ODataConventionModelBuilder() {
Namespace = "Namespace",
ContainerName = "Container", // no idea what this is for
};
builder.EntitySet<ODataEntity>("objects");
var model = (EdmModel) builder.GetEdmModel();
IODataTypeMapper mapper = model.GetTypeMapper();
foreach (var edmType in model.SchemaElements.OfType<EdmEntityType>()) {
if (edmType.Name == nameof(ODataEntity)) {
foreach (var field in customFields) {
var typeRef = mapper.GetEdmTypeReference(model, field.Type);
edmType.AddStructuralProperty(field.FieldName, typeRef);
}
}
}
return model;
}
Step 2: Obtain the custom fields
Normally you call mvc.AddOData in ConfigureServices in Startup.cs, passing it a lambda that will create the IEdmModel. But wait, are your custom field definitions stored in your database? If so, how can you access the database from inside ConfigureServices? Don't worry: the lambda passed to AddOData is called after Configure, so it is possible to access the database or any other required service with some slightly hacky code. This code also installs a class called ODataResourceSerializerForCustomFields which is the subject of step 3:
public void ConfigureServices(IServiceCollection services)
{
...
IMvcBuilder mvc = services.AddControllers(...);
// OData Configuration
mvc.AddOData(opt => {
var scopeProvider = _serviceProvider!.CreateScope().ServiceProvider;
var cfdm = scopeProvider.GetRequiredService<CustomFieldDefManager>();
var edmModel = GetEdmModel(cfdm.GetAll());
opt.AddRouteComponents("odata", edmModel, services => {
services.AddScoped<ODataResourceSerializer,
ODataResourceSerializerForCustomFields>();
}).Select().Filter().OrderBy().Count().Expand().SkipToken();
});
...
}
IServiceProvider? _serviceProvider;
public void Configure(IApplicationBuilder app, ...)
{
_serviceProvider = app.ApplicationServices;
...
}
Of course, this code is not dynamic: it generates the EdmModel only once on startup. I will figure out later how to make this dynamic.
Step 3: Stop it from crashing
Microsoft.AspNetCore.OData.dll isn't designed to support custom fields in the EdmModel. It's a young product, you understand, only version 8.0. As soon as you add a custom field, you'll get an InvalidOperationException like "The EDM instance of type '[ODataEntity Nullable=True]' is missing the property 'ExampleCustomField'., because the library assumes that all properties in the EdmModel are real CLR properties.
I found a way around this problem by overriding a few methods of ODataResourceSerializer. But first, define an IHasCustomFields interface and make sure that any OData entity with custom fields implements this interface:
public interface IHasCustomFields
{
public Dictionary<string, object?> CustomFields { get; set; }
}
Now let's add ODataResourceSerializerForCustomFields, which uses special behavior when this interface is present.
/// <summary>
/// This class modifies the behavior of ODataResourceSerializer to
/// stop it from crashing when the EdmModel contains custom fields.
/// Note: these modifications are designed for simple custom fields
/// (e.g. string, bool, DateTime).
/// </summary>
public class ODataResourceSerializerForCustomFields : ODataResourceSerializer
{
public ODataResourceSerializerForCustomFields(IODataSerializerProvider serializerProvider)
: base(serializerProvider) { }
IHasCustomFields? _hasCustomFields;
HashSet<string>? _realProps;
public override Task WriteObjectInlineAsync(
object graph, IEdmTypeReference expectedType,
ODataWriter writer, ODataSerializerContext writeContext)
{
_hasCustomFields = null;
if (graph is IHasCustomFields hasCustomFields) {
_hasCustomFields = hasCustomFields;
var BF = BindingFlags.Public | BindingFlags.Instance;
_realProps = graph.GetType().GetProperties(BF).Select(p => p.Name).ToHashSet();
}
return base.WriteObjectInlineAsync(graph, expectedType, writer, writeContext);
}
public override ODataResource CreateResource(
SelectExpandNode selectExpandNode, ResourceContext resourceContext)
{
return base.CreateResource(selectExpandNode, resourceContext);
}
public override ODataProperty CreateStructuralProperty(
IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
{
// Bypass tne base class if the current property doesn't physically exist
if (_hasCustomFields != null && !_realProps!.Contains(structuralProperty.Name)) {
_hasCustomFields.CustomFields.TryGetValue(structuralProperty.Name, out object? value);
return new ODataProperty {
Name = structuralProperty.Name,
Value = ToODataValue(value)
};
}
return base.CreateStructuralProperty(structuralProperty, resourceContext);
}
public static ODataValue ToODataValue(object? value)
{
if (value == null)
return new ODataNullValue();
if (value is DateTime date)
value = (DateTimeOffset)date;
return new ODataPrimitiveValue(value);
}
// The original implementation of this method can't be prevented from
// crashing, so replace it with a modified version based on the original
// source code. This version is simplified to avoid calling `internal`
// methods that are inaccessible, but as a result I'm not sure that it
// behaves quite the same way. If properties that aren't in the EdmModel
// aren't needed in the output, the method body is optional and deletable.
public override void AppendDynamicProperties(ODataResource resource,
SelectExpandNode selectExpandNode, ResourceContext resourceContext)
{
if (_hasCustomFields == null) {
base.AppendDynamicProperties(resource, selectExpandNode, resourceContext);
return;
}
if (!resourceContext.StructuredType.IsOpen || // non-open type
(!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) {
return;
}
IEdmStructuredType structuredType = resourceContext.StructuredType;
IEdmStructuredObject structuredObject = resourceContext.EdmObject;
object value;
if (structuredObject is IDelta delta) {
value = ((EdmStructuredObject)structuredObject).TryGetDynamicProperties();
} else {
PropertyInfo dynamicPropertyInfo = resourceContext.EdmModel.GetDynamicPropertyDictionary(structuredType);
if (dynamicPropertyInfo == null || structuredObject == null ||
!structuredObject.TryGetPropertyValue(dynamicPropertyInfo.Name, out value) || value == null) {
return;
}
}
IDictionary<string, object> dynamicPropertyDictionary = (IDictionary<string, object>)value;
// Build a HashSet to store the declared property names.
// It is used to make sure the dynamic property name is different from all declared property names.
HashSet<string> declaredPropertyNameSet = new HashSet<string>(resource.Properties.Select(p => p.Name));
List<ODataProperty> dynamicProperties = new List<ODataProperty>();
// To test SelectedDynamicProperties == null is enough to filter the dynamic properties.
// Because if SelectAllDynamicProperties == true, SelectedDynamicProperties should be null always.
// So `selectExpandNode.SelectedDynamicProperties == null` covers `SelectAllDynamicProperties == true` scenario.
// If `selectExpandNode.SelectedDynamicProperties != null`, then we should test whether the property is selected or not using "Contains(...)".
IEnumerable<KeyValuePair<string, object>> dynamicPropertiesToSelect =
dynamicPropertyDictionary.Where(x => selectExpandNode.SelectedDynamicProperties == null || selectExpandNode.SelectedDynamicProperties.Contains(x.Key));
foreach (KeyValuePair<string, object> dynamicProperty in dynamicPropertiesToSelect) {
if (string.IsNullOrEmpty(dynamicProperty.Key))
continue;
if (declaredPropertyNameSet.Contains(dynamicProperty.Key))
continue;
dynamicProperties.Add(new ODataProperty {
Name = dynamicProperty.Key,
Value = ToODataValue(dynamicProperty.Value)
});
}
if (dynamicProperties.Count != 0)
resource.Properties = resource.Properties.Concat(dynamicProperties);
}
}
Q. 1
I'm trying to push an entity into CRM. The json looks like this:
{
"sub_AssignedToCompanyId#odata.bind": "/accounts(f52f9dd7-35e5-e711-813b-480fcff40721)"
}
Now, my C# class has a property on it called AssignedToCompany, looks like this:
[JsonProperty(PropertyName = "sub_AssignedToCompanyId#odata.bind")]
public int AssignedToCompany{ get; set; }
What I want is a way to serialize it to sub_AssignedToCompanyId#odata.bind (which i've done using JsonProperty("sub_AssignedToCompanyId#odata.bind"). But then the swagger definition doesn't look good. I want the swagger definition of this property to say "assignedToCompanyId". Is there anyway to do this? I'm using .Net Framework Web API.
Q.2
And, is there a way that I can translate this input:
f52f9dd7-35e5-e711-813b-480fcff40721
to this output:
/accounts(f52f9dd7-35e5-e711-813b-480fcff40721)
automatically during serialization?
You can use IOperationFilter, ISchemaFilter or IDocumentFilter. Whichever best suits your needs.
An example of an ISchemaFilter could be:
public class ODataTypeSchemaFilter : ISchemaFilter
{
private const string propToRename = "sub_AssignedToCompanyId#odata.bind";
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type, type)
{
if (type == typeof(MyObject))
{
// adding new schema property, and removing the old one
schema.properties.Add(nameof(MyObject.AssignedToCompany), schema.properties[propToRename]);
schema.properties.Remove(propToRename);
}
}
}
public class MyObject
{
[JsonProperty(PropertyName = "sub_AssignedToCompanyId#odata.bind")]
public int AssignedToCompany { get; set; }
}
This is a powerful feature, changing the schema could potentially cause other problems if not done properly. See Swagger documentation for more info. I've used all three in my experience, just find which suits your needs best.
You can try also this one. The previous answers did not work for me but this did.
However I recommend to use the desired names in you model's property names because then maybe you will have to address also the problem of deserializing the json to the objects when a request happens.
public class CustomNameSchema : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
if (schema.Properties.Any())
{
foreach (var propType in context.Type.GetProperties().Where(x => x.CustomAttributes != null && x.CustomAttributes.Any()))
{
var schemaProp = schema.Properties.FirstOrDefault(x => x.Key.Equals(propType.Name, StringComparison.InvariantCultureIgnoreCase));
string newName = propType.GetCustomAttribute<DataMemberAttribute>()?.Name;
if (string.IsNullOrEmpty(newName))
continue;
if (schemaProp.Value.Enum != null && schemaProp.Value.Enum.Any())
{
for (int i = 0; i < schemaProp.Value.Enum.Count; i++)
{
OpenApiString curr = schemaProp.Value.Enum[i] as OpenApiString;
var memberInfo = propType.PropertyType.GetMember(curr.Value).FirstOrDefault();
string newValue = memberInfo.GetCustomAttribute<EnumMemberAttribute>()?.Value;
if (string.IsNullOrWhiteSpace(newValue))
continue;
OpenApiString newStr = new OpenApiString(newValue);
schemaProp.Value.Enum.Remove(curr);
schemaProp.Value.Enum.Insert(0, newStr);
}
}
var newSchemaProp = new KeyValuePair<string, OpenApiSchema>(newName, schemaProp.Value);
schema.Properties.Remove(schemaProp);
schema.Properties.Add(newSchemaProp);
}
}
var objAttribute = context.Type.GetCustomAttribute<DataContractAttribute>();
if (objAttribute != default && objAttribute?.Name?.Length > 0)
{
schema.Title = objAttribute.Name;
}
}
}
For me, just add [System.Text.Json.Serialization.JsonPropertyName("property")] resolved the problem
using System.Text.Json.Serialization;
[Serializable]
public class Foo
{
[JsonPropertyName("bar")]
public long Bar{ get; set; }
}
As per Answer of #bytedev :
https://stackoverflow.com/a/60276145/12417530
the following solution worked for me :
It would appear that full support for NewtonSoft JSON.net from version 5.0.0 of Swashbuckle/Swagger is provided through a separate package.
To get this working:
Install the nuget package Swashbuckle.AspNetCore.Newtonsoft version 5.0.0+
In Startup.ConfigureServices() make a call for the support:
services.AddSwaggerGenNewtonsoftSupport();
Further info can be found here.
I have an MVC Web API Controller that exposes a method:
[HttpPost]
public JsonResult<CustomClass> Details(RequestClass request)
{
var encoding = Encoding.GetEncoding("iso-8859-1");
var settings = new Newtonsoft.Json.JsonSerializerSettings();
return Json(_InputService.CustomClassGet(request.Id), settings, encoding);
}
CustomClass is a really complex class wich contains several levels of objects of different classes. Objects from these classes are somehow built in another part of the code using a mapper that relies on Newtonsoft Json.
I just got a request to modify my code to change some of CustomClass property names (along the whole tree). My first approach was to create another set of classes, so I could have one for receiving data and other for exposing data with a converter in the middle but there are so many classes in the structure and they are so complex that it would consume a lot of effort. Also, during the process of converting from input to output classes, it would require twice the memory to hold 2 copies of the same exact data.
My second approach was using JsonProperty(PropertyName ="X") to change the resulting json BUT, as the input also relies in Newtonsoft Json, I completely broke the input process.
My next approach was to create a custom serializer and user [JsonConverter(typeof(CustomCoverter))] attribute BUT it changes the way CustomClass is serialized everywhere and I can't change the way the rest of the API responds, just some specific methods.
So, the question is... does anyone imagine a way to change the way my CustomClass is serialized just in certain methods?
This is the final solution:
1) Created a custom Attribute and decorated the properties that I needed its name changed in the serialized Json:
[AttributeUsage(AttributeTargets.Property)]
class MyCustomJsonPropertyAttribute : Attribute
{
public string PropertyName { get; protected set; }
public MyCustomJsonPropertyAttribute(string propertyName)
{
PropertyName = propertyName;
}
}
2) Decordated the properties accordingly
class MyCustomClass
{
[MyCustomJsonProperty("RenamedProperty")]
public string OriginalNameProperty { get; set; }
}
3) Implemented a Custom Resolver using reflection to remap the property name for those properties decorated with MyCustomJsonPropertyAttribute
class MyCustomResolver : DefaultContractResolver
{
public MyCustomResolver()
{
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty prop = base.CreateProperty(member, memberSerialization);
var attr = member.GetCustomAttribute(typeof(MyCustomJsonPropertyAttribute));
if (attr!=null)
{
string jsonName = ((MyCustomJsonPropertyAttribute)attr).PropertyName;
prop.PropertyName = jsonName;
}
return prop;
}
}
4) Implemented the method in the controller like this:
[HttpPost]
public JsonResult<MyCustomClass> Details(RequestClass request)
{
var encoding = Encoding.GetEncoding("iso-8859-1");
var settings = new Newtonsoft.Json.JsonSerializerSettings()
{
ContractResolver = new MyCustomResolver()
};
return Json(_InputService.CustomClassGet(request.Id), settings, encoding);
}
Thanks a lot to dbc for pointing me the way.
I'm working on the business model for my first project(please excuse me if someone finds my code lack of quality, important thing is i'm making progress). I'm stuck trying to find the reason for a bug. I'm creating a view which rely on reflection of properties and custom attributes. I get a null reference exception when i use the PropertyInfo.GetCustomAttribute for the second time on a "property's property". Why does my second call return null. As you can see I have applied the attribute on the property(_TopSchools) which i invoke method on.
public class EducationFilter : Filter
{
[FilterAttribute(FilterType.Child, "Topschools")]//I cant get this attr!
public TopSchoolFilter _TopSchool { get; set; }
}
public class TopSchoolFilter :BooleanFilter
{
}
public class Filters
{
[FilterAttribute(FilterType.Parent, "Education")] //This i can...
public EducationFilter _EducationFilter { get; set; }
public Filters(EducationFilter educationFilter)
{
this._EducationFilter = educationFilter;
}
}
public StackLayout GenerateFilterView(PropertyInfo p,TestModel vm)
{
StackLayout tempStack = new StackLayout();
**FilterAttribute filterAttr = p.GetCustomAttribute<FilterAttribute>();**//This returns the attr instance
IEnumerable<PropertyInfo> filterProperties = p.PropertyType.GetRuntimeProperties();
foreach (PropertyInfo p1 in filterProperties)
{
**FilterAttribute filterAttr1 = p1.GetCustomAttribute<FilterAttribute>();**//But not this one, i get null
If GetCustomAttribute<T>() returns null then that means the custom attribute provider (the property in this case) doesn't have an attribute of that type. If you are only interested in properties with this attribute, you can just skip over the properties without the attribute.
if (filterAttr1 == null) {
continue;
}
<TL;DR>
At a minimum, I'm looking for a way to conditionally exclude certain properties on the resource from being included in the response on a per-call basis (See fields below).
Ideally, I'd like to implement a REST service with ServiceStack that supports all the major points below.
UPDATE
While I really like ServiceStack's approach in general and would prefer to use it if possible, if it isn't particularly well suited towards these ideas I'd rather not bend over backwards bastardizing it to make it work. If that's the case, can anyone point to another c# framework that might be more appropriate? I'm actively exploring other options myself, of course.
</TD;DR>
In this talk entitled Designing REST + JSON APIs, the presenter describes his strategy for Resource References (via href property on resources) in JSON. In addition to this, he describes two query parameters (fields and expand) for controlling what data is included the response of a call to a REST service. I've been trying without success to dig into the ServiceStack framework to achieve support for fields in particular but have thus far been unsuccessful. Is this currently possible in ServiceStack? Ideally the solution would be format agnostic and would therefore work across all of ServiceStack's supported output formats. I would imagine expand would follow the same strategy.
I'll describe these features here but I think the talk at the link does a better job of explaining them.
Lets say we have an Profiles resource with the following properties: givenName, surname, gender, and favColor. The Profiles resource also includes a list of social networks the user belongs to in the socialNetworks property.
href - (42:22 in video) Every resource includes a full link to it on the REST service. A call to GET /profiles/123 would return
{
"href":"https://host/profiles/123",
"givenName":"Bob",
"surname":"Smith",
"gender":"male",
"favColor":"red",
"socialNetworks": {
"href":"https://host/socialNetworkMemberships?profileId=123"
}
}
Notice that the socialNetworks property returns an object with just the href value populated. This keeps the response short and focused while also giving the end user enough information to make further requests if desired. The href property, used across the board in this manor, makes it easy (conceptually anyway) to reuse resource data structures as children of other resources.
fields - (55:44 in video) Query string parameter that instructs the server to only include the specified properties of the desired resource in the REST response.
A normal response from GET /profiles/123 would include all the properties of the resource as seen above. When the fields query param is included in the request, only the fields specified are returned. 'GET /propfiles/123?fields=surname,favColor' would return
{
"href":"https://host/profiles/123",
"surname":"Smith",
"favColor":"red"
}
expand - (45:53 in video) Query string parameter that instructs the server to flesh out the specified child resources in the result. Using our example, if you were to call GET /profiles/123?expand=socialNetworks you might receive something like
{
"href":"https://host/profiles/123",
"givenName":"Bob",
"surname":"Smith",
"gender":"male",
"favColor":"red",
"socialNetworks": {
"href":"https://host/socialNetworkMemberships?profileId=123",
"items": [
{
"href":"https://host/socialNetworkMemberships/abcde",
"siteName":"Facebook",
"profileUrl":"http://www.facebook.com/..."
},
...
]
}
}
So...in my opinion ServiceStack's best feature is that it makes sending, receiving and handling POCOs over HTTP super easy. How you set up the POCOs and what you do in between (within the 'Service') is up to you. Does SS have opinions? Yes. Do you have to agree with them? No. (But you probably should :))
I think expanding on something like below would get you close to how you want to handle your api. Probably not the best example of ServiceStack but the ServiceStack code/requirements are barely noticeable and don't get in your way (AppHost configure not shown). You could probably do something similar in other .NET Frameworks (MVC/Web API/etc) but, in my opinion, won't look as much like straight C#/.NET code as with ServiceStack.
Request classes
[Route("/Profiles/{Id}")]
public class Profiles
{
public int? Id { get; set; }
}
[Route("/SocialNetworks/{Id}")]
public class SocialNetworks
{
public int? Id { get; set; }
}
Base Response class
public class BaseResponse
{
protected virtual string hrefPath
{
get { return ""; }
}
public string Id { get; set; }
public string href { get { return hrefPath + Id; } }
}
Classes from example
public class Profile : BaseResponse
{
protected override string hrefPath { get { return "https://host/profiles/"; } }
public string GivenName { get; set; }
public string SurName { get; set; }
public string Gender { get; set; }
public string FavColor { get; set; }
public List<BaseResponse> SocialNetworks { get; set; }
}
public class SocialNetwork: BaseResponse
{
protected override string hrefPath { get { return "https://host/socialNetworkMemberships?profileId="; }}
public string SiteName { get; set; }
public string ProfileUrl { get; set; }
}
Services
public class ProfileService : Service
{
public object Get(Profiles request)
{
var testProfile = new Profile { Id= "123", GivenName = "Bob", SurName = "Smith", Gender = "Male", FavColor = "Red",
SocialNetworks = new List<BaseResponse>
{
new SocialNetwork { Id = "abcde", SiteName = "Facebook", ProfileUrl = "http://www.facebook.com/"}
}
};
if (!String.IsNullOrEmpty(this.Request.QueryString.Get("fields")) || !String.IsNullOrEmpty(this.Request.QueryString.Get("expand")))
return ServiceHelper.BuildResponseObject<Profile>(testProfile, this.Request.QueryString);
return testProfile;
}
}
public class SocialNetworkService : Service
{
public object Get(SocialNetworks request)
{
var testSocialNetwork = new SocialNetwork
{
Id = "abcde",
SiteName = "Facebook",
ProfileUrl = "http://www.facebook.com/"
};
if (!String.IsNullOrEmpty(this.Request.QueryString.Get("fields")) || !String.IsNullOrEmpty(this.Request.QueryString.Get("expand")))
return ServiceHelper.BuildResponseObject<SocialNetwork>(testSocialNetwork, this.Request.QueryString);
return testSocialNetwork;
}
}
Reflection Helper Class
public static class ServiceHelper
{
public static object BuildResponseObject<T>(T typedObject, NameValueCollection queryString) where T: BaseResponse
{
var newObject = new ExpandoObject() as IDictionary<string, object>;
newObject.Add("href", typedObject.href);
if (!String.IsNullOrEmpty(queryString.Get("fields")))
{
foreach (var propertyName in queryString.Get("fields").Split(',').ToList())
{
//could check for 'socialNetwork' and exclude if you wanted
newObject.Add(propertyName, typedObject.GetType().GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).GetValue(typedObject, null));
}
}
if (!String.IsNullOrEmpty(queryString.Get("expand")))
{
foreach (var propertyName in queryString.Get("expand").Split(',').ToList())
{
newObject.Add(propertyName, typedObject.GetType().GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).GetValue(typedObject, null));
}
}
return newObject;
}
}
Usually you can control the serialization of your DTOs by setting the DataMember attributes. With those attributes you can control if the property should have defaults or not.
Meaning if you simply do not define the property of the object you want to return, it should not be serialized and therefore will not be shown in the resulting Json.
ServiceStack internally uses the standard DataContract...Serializer, so this should be supported
Otherwise you could also make use of dynamic objects and simply compose your object at runtime, serialize it and send it back.
Here is a very very basic example:
var seri = JsonSerializer.Create(new JsonSerializerSettings() { });
using (var textWriter = new StringWriter())
{
var writer = new JsonTextWriter(textWriter);
dynamic item = new { Id = id };
seri.Serialize(writer, item);
return textWriter.ToString();
}