Web API OData Typeless support for $select query option - asp.net-web-api-odata

I am working with the new typeless support in ASP.NET Web API 2 OData. I am interested in providing support for the $select query option. How do I omit the structural properties from the EdmEntityObject that were not selected by the $select query option?
The following is a sample of the Web API Configuration for a very simple example working with a typeless model.
public static IEdmModel BuildEdmModel()
{
var model = new EdmModel();
var container = new EdmEntityContainer("Model", "OData");
var product = new EdmEntityType("Model", "Product");
var productKey = product.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Guid);
product.AddKeys(productKey);
product.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String);
product.AddStructuralProperty("Price", EdmPrimitiveTypeKind.Double);
model.AddElement(product);
model.AddElement(container);
model.SetIsDefaultEntityContainer(container, true);
container.AddEntitySet("Products", product);
return model;
}
public static void Register(HttpConfiguration config)
{
config.Routes.MapODataRoute("ODataRoute", "odata", BuildEdmModel());
}
The following is a partial snippet from a simple ODataController
public EdmEntityObjectCollection Get()
{
var path = Request.GetODataPath();
var edmType = path.EdmType;
var collectionType = edmType as IEdmCollectionType;
var entityType = collectionType.ElementType.AsEntity();
var entitySetName = entityType.EntityDefinition().Name;
var model = Request.GetEdmModel();
var queryContext = new ODataQueryContext(Request.GetEdmModel(), entityType.Definition);
var queryOptions = new ODataQueryOptions(queryContext, Request);
return GetData(Request.GetEdmModel(), queryOptions);
}
public static EdmEntityObjectCollection GetData(IEdmModel edmModel, ODataQueryOptions queryOptions)
{
var selectedPropertyNames = new string[0];
// determine the selected property names
if (queryOptions.SelectExpand != null && queryOptions.SelectExpand.SelectExpandClause != null && (!queryOptions.SelectExpand.SelectExpandClause.AllSelected || queryOptions.SelectExpand.SelectExpandClause.SelectedItems.OfType<WildcardSelectItem>().Any()))
{
selectedPropertyNames = queryOptions.SelectExpand.RawSelect.Split(',');
}
// TODO: Now that we have the selected properties, how do I remove the structural properties from the EdmEntityObject that were not selected by the $select query option?
var productSchemaType = edmModel.FindDeclaredType(string.Format("{0}.Product", "Model"));
var productEntityType = productSchemaType as IEdmEntityType;
var productEntityTypeReference = new EdmEntityTypeReference(productEntityType, true);
var products = new EdmEntityObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(productEntityTypeReference), true));
var productWindows = new EdmEntityObject(productEntityTypeReference);
productWindows.TrySetPropertyValue("ID", new Guid("52D811A0-9065-4B83-A2E8-0248FBA9FBF5"));
productWindows.TrySetPropertyValue("Name", "Microsoft Windows 8");
productWindows.TrySetPropertyValue("Price", 179.99);
var productOffice = new EdmEntityObject(productEntityTypeReference);
productOffice.TrySetPropertyValue("ID", new Guid("CB39EBD0-4751-4D5F-A76C-78FCC7A9CE1A"));
productOffice.TrySetPropertyValue("Name", "Microsoft Office 2013");
productOffice.TrySetPropertyValue("Price", 399.99);
products.Add(productWindows);
products.Add(productOffice);
return products;
}
This will output:
{
"odata.metadata":"http://localhost:59511/odata/$metadata#Products","value":[
{
"ID":"52d811a0-9065-4b83-a2e8-0248fba9fbf5","Name":"Microsoft Windows 8","Price":179.99
},{
"ID":"cb39ebd0-4751-4d5f-a76c-78fcc7a9ce1a","Name":"Microsoft Office 2013","Price":399.99
}
]
}
If the user applies a $select query option, for example /odata/Products?$select=Name. This should result in the following output:
{
"odata.metadata":"http://localhost:59511/odata/$metadata#Products","value":[
{
"Name":"Microsoft Windows 8"
},{
"Name":"Microsoft Office 2013"
}
]
}
Any help would be greatly appreciated

The following works for me.
var oDataProperties = Request.ODataProperties()
oDataProperties.SelectExpandClause = queryOptions.SelectExpand.SelectExpandClause;
The extension function is in System.web.OData.dll v5.2.0.0.

Here's my code.
private void ApplySelectExpand(SelectExpandQueryOption selectExpand)
{
if (selectExpand != null)
{
Request.SetSelectExpandClause(selectExpand.SelectExpandClause);
}
}

There are two ways to solve your problem if I understood your question correctly.
Call the ApplyTo method of ODataQueryOptions on the IQueryable result
return queryOptions.ApplyTo(products);
Add attribute Queryable on the GetData method and let WebAPI handles the query option
[Queryable]
public static EdmEntityObjectCollection GetData(IEdmModel edmModel)
{...}

Related

How to configure Swashbuckle to ignore property on model for a specific api version only

I needed to add a property to a model and I have implemented what is suggested in the selected answer here and it's working in that it removes the property I have tagged with SwaggerIgnorePropertyAttribute attribute of the model.
My question is , if I have several API versions of my application,how to remove it from the swagger doc/schema for certain versions? I only care not to see the added property in swagger. I only really have 1 version of the model across the application even though I have several version of the app.
I added the version like this:
services.AddSwaggerGen(
swaggerOptions =>
{
swaggerOptions.SwaggerDoc(
"v1",
new Info
{
Title = "Titlebla1",
Description = "bla1",
Version = "v1"
});
swaggerOptions.SwaggerDoc(
"v2",
new Info
{
Title = "Titlebla2",
Description = "bla2",
Version = "v2"
});
etc
I know that I can find the version by using SwaggerDocument and doing something like this: swaggerDoc.Info.Version but how can I get access to SwaggerDocument from the Apply in my SwaggerExcludePropertySchemaFilter class below ?
private class SwaggerExcludePropertySchemaFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var excludedProperties = context.SystemType.GetProperties().Where(t => t.GetCustomAttribute<SwaggerIgnorePropertyAttribute>() != null);
foreach (var excludedProperty in excludedProperties)
{
var propertyToRemove = schema.Properties.Keys.SingleOrDefault(x => string.Equals(x, excludedProperty.Name, StringComparison.OrdinalIgnoreCase));
if (propertyToRemove != null)
{
schema.Properties.Remove(propertyToRemove);
}
}
}
}
Try use IDocumentFilter , see example:
public class CustomSwaggerFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var nonRequiredMYPropertyAPIs = swaggerDoc.Paths
.Where(x => !x.Key.ToLower().Contains("v1") /*the version you want to remove the property */)
.ToList();
nonRequiredMYPropertyAPIs.ForEach(x => {
swaggerDoc.Components.Schemas["YOUR_CLASS_MODEL"]
.Properties.Remove("PROPERTY_NAME_YOU_WANT_TO_IGNORE");
});
}
}
Don't forget register the filter:
c.DocumentFilter<CustomSwaggerFilter>();

.Resx file is empty when using it in .cs file

I am trying to read data from .resx file. It works fine in views, but having trouble when using it in .cs.
I am getting this runtime error:
Object reference not set to an instance of an object
MailComposer.cs
IStringLocalizer<SharedResources> SharedLocalizer;
public void SendActivityCreated (Activity entity) {
var path = Path.Combine (environment.ContentRootPath, "wwwroot", "mail_templates", "activity_created", "index.html");
var template = File.ReadAllText (path);
template = template.Replace ("##ID##", entity.ID.ToString ());
var x = SharedLocalizer["NewActivity"]; // Getting "Object reference not set to an instance of an object" here
var title = $"Platform.Ge - {x} #{entity.ID}";
var responsibleEmail = template.Replace ("##USER##", entity.Responsible.Name);
emailSender.SendEmailAsync (entity.Responsible.Email, title, responsibleEmail);
}
Startup.cs
services.Configure<RequestLocalizationOptions> (opts => {
var supportedCultures = new [] {
new CultureInfo ("en"),
new CultureInfo ("ka"),
new CultureInfo ("ru")
};
opts.DefaultRequestCulture = new RequestCulture ("ka");
// Formatting numbers, dates, etc.
opts.SupportedCultures = supportedCultures;
// UI strings that we have localized.
opts.SupportedUICultures = supportedCultures;
});
I have SharedResources.ka.resx and SharedResources.en.resx files. How do I get data from these two files in SharedLocalizer instance of MailComposer.cs?
Solution 1 :
Inject SharedResources in MailComposer.cs :
IStringLocalizer<SharedResource> SharedLocalizer;
public MailComposer(IStringLocalizer<SharedResource> _SharedLocalizer)
{
SharedLocalizer = _SharedLocalizer;
}
Add below line in Startup.cs :
services.AddScoped<MailComposer>();
Inject MailComposer in the place where you want to call the SendActivityCreated function :
private readonly MailComposer _mailComposer;
public HomeController(MailComposer mailComposer){
_mailComposer = mailComposer;
}
And use like :
_mailComposer.SendActivityCreated(entity);
Solution 2 :
Inject SharedResources in MailComposer.cs :
IStringLocalizer<SharedResource> SharedLocalizer;
public MailComposer(IStringLocalizer<SharedResource> _SharedLocalizer)
{
SharedLocalizer = _SharedLocalizer;
}
Inject IStringLocalizer<SharedResource> in the place where you want to call the SendActivityCreated function :
private readonly IStringLocalizer<SharedResource> _localizer;
public HomeController(IStringLocalizer<SharedResource> localizer){
_localizer = localizer;
}
And use like :
MailComposer a = new MailComposer(_localizer);
a.SendActivityCreated(entity);

HttpParameterBinding without code coverage

OpenCover On Cake script does not detect coverage on my Owin.Testing usage applying HttpPArameterBiding to some ApiController action parameter.
I have Created a new type of my ApiController that as an action with my ParameterBindingAttribute that I called FromHeaderAttribute. After that I created my Owin Test Server and respective HttpClient and did the requests and the proper asserts to validate that the Binding is working properly. The tests pass with sucess.
This is my unit tests Cake Task
Task("UnitTests")
.IsDependentOn("Build")
.IsDependentOn("RestoreNugets")
.DoesForEach(GetFiles($"{testsPath}/**/*.csproj"), (file) =>
{
var openCoverSettings = new OpenCoverSettings
{
OldStyle = true,
MergeOutput = true,
Register = "user",
LogLevel = OpenCoverLogLevel.Verbose,
ArgumentCustomization = args => args.Append("-coverbytest:*.Tests.dll").Append("-mergebyhash")
}
.WithFilter("+[AppRootName.*]*");
var projectName = file.GetFilename().ToString().Replace(".csproj",string.Empty);
var dotNetTestSettings = new DotNetCoreTestSettings
{
Configuration = "Release",
DiagnosticOutput = true,
Verbosity = DotNetCoreVerbosity.Normal,
ArgumentCustomization = (args)=>
{
args.Append($"--logger \"trx;LogFileName={projectName}-TestsResults.trx\"");
args.Append("--filter \"TestCategory=Unit|Category=Unit\"");
return args;
}
};
OpenCover(context => context.DotNetCoreTest(file.FullPath, dotNetTestSettings), new FilePath($"CoverageResults.xml"), openCoverSettings);
})
.Finally(()=>
{
Information($"Copying test reports to ${outputDir}/TestsResults .... ");
CopyFiles($"{testsPath}/**/TestResults/*.trx",$"{outputDir}/TestsResults");
ReportGenerator($"*-CoverageResults.xml", $"{outputDir}/Reports");
});
this is my XUnit test:
[Fact]
[Trait("Category", "Unit")]
public async Task WhenHeadersArePresent_SettingsShouldBeSetted()
{
HttpConfiguration configuration = new HttpConfiguration();
var container = new SimpleInjector.Container();
Mock<IApiControllerValidation> mockValidationInterface = new Mock<IApiControllerValidation>();
ManualResetEvent resetEvent = new ManualResetEvent(false);
Settings settingsReceived = null;
mockValidationInterface.Setup((validator) => validator.Assert(It.IsAny<object>(), It.IsAny<object>(), It.IsAny<IDictionary<string, string>>(), It.IsAny<IHttpActionResult>()))
.Callback<object, object, IDictionary<string, string>, IHttpActionResult>((header, body, parameters, result) =>
{
settingsReceived = header as Settings;
resetEvent.Set();
});
container.RegisterInstance(mockValidationInterface.Object);
using (var server = TestServer.Create(app =>
{
configuration.DependencyResolver = new SimpleInjectorWebApiDependencyResolver(container);
configuration.MapHttpAttributeRoutes();
app.Use((owinContext, nextHandler)=> nextHandler());
app.UseWebApi(configuration);
}))
{
var client = server.HttpClient;
client.DefaultRequestHeaders.Add("header1", new List<string>() { "headervalue1" } );
client.DefaultRequestHeaders.Add("header2", new List<string>() { "headervalue2" });
var result = await client.PostAsync<Payload>("optionalHeader", new Payload("value1"), new JsonMediaTypeFormatter());
Assert.Equal(HttpStatusCode.OK,result.StatusCode);
};
resetEvent.WaitOne();
Assert.NotNull(settingsReceived);
Assert.Equal("headervalue1", settingsReceived.Header1);
Assert.Equal("headervalue2", settingsReceived.Header2);
}
And this is my Api Action were I want to test the FromHEader attribute that I have implement.
[HttpPost]
[Route("optionalHeader",Name = "PostValidation")]
public IHttpActionResult OptionalHeaders([FromHeader]Settings settings, [FromBody]Payload payload)
{
var result = Ok();
validation.Assert(settings,payload, null, result);
return result;
}
I expect that the code coverage of the test detects the usage of This type but its not because the report is showing 0 code coverage on my type.
I figured out what was the problem, and it was not related to anything related to asp.net framework HttpParameterBinding component.
instead of execute the code cover like this:
OpenCover(context => context.DotNetCoreTest(file.FullPath, dotNetTestSettings), new FilePath($"CoverageResults.xml"), openCoverSettings);
I changed that to be like this:
OpenCover(tool => {
tool.XUnit2($"{testsPath}/**/**/**/**/{projectName}.dll",xUnit2Settings);
}, new FilePath("./OpenCoverCoverageResults.xml"),openCoverSettings);
Also the Build must be done with configuration in debug mode so the OpenCover can use the pdbs.
The only thing that I still dont like is the path to the dlls to be explicit by the number of levels that are explicit and I also did not want to copy the dlls because that will take more time.

Rendering #Html.Action("actionName","controllerName") at runtime , fetching from database in MVC4

My requirement is to fetch html data from database and render it on view. But if that string contains #Html.Action("actionName","controllerName"), i need to call perticular controller action method also.
I am rendering my html on view using #Html.Raw().
Eg: Below is the html string stored in my database
'<h2> Welcome To Page </h2> <br/> #Html.Action("actionName", "controllerName")'
So when it render the string, it execute mentioned controller and action too.
Any help will be appreciated.
You can try RazorEngine to allow string template in razor executed.
For example, sample code from the project site http://antaris.github.io/RazorEngine/:
using RazorEngine;
using RazorEngine.Templating; // For extension methods.
string template = "Hello #Model.Name, welcome to RazorEngine!";
var result =
Engine.Razor.RunCompile(template, "templateKey", null, new { Name = "World" });
But there is one catch, Html and Url helpers are defined in the Mvc framework, hence it is not supported by default.
I will suggest you try to create your template by passing model so that you don't have to use #Html.Action.
If you can not avoid it, then there is possible a solution suggested by another so answer https://stackoverflow.com/a/19434112/2564920:
[RequireNamespaces("System.Web.Mvc.Html")]
public class HtmlTemplateBase<T>:TemplateBase<T>, IViewDataContainer
{
private HtmlHelper<T> helper = null;
private ViewDataDictionary viewdata = null;
public HtmlHelper<T> Html
{
get
{
if (helper == null)
{
var writer = this.CurrentWriter; //TemplateBase.CurrentWriter
var context = new ViewContext() { RequestContext = HttpContext.Current.Request.RequestContext, Writer = writer, ViewData = this.ViewData };
helper = new HtmlHelper<T>(vcontext, this);
}
return helper;
}
}
public ViewDataDictionary ViewData
{
get
{
if (viewdata == null)
{
viewdata = new ViewDataDictionary();
viewdata.TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = string.Empty };
if (this.Model != null)
{
viewdata.Model = Model;
}
}
return viewdata;
}
set
{
viewdata = value;
}
}
public override void WriteTo(TextWriter writer, object value)
{
if (writer == null)
throw new ArgumentNullException("writer");
if (value == null) return;
//try to cast to RazorEngine IEncodedString
var encodedString = value as IEncodedString;
if (encodedString != null)
{
writer.Write(encodedString);
}
else
{
//try to cast to IHtmlString (Could be returned by Mvc Html helper methods)
var htmlString = value as IHtmlString;
if (htmlString != null) writer.Write(htmlString.ToHtmlString());
else
{
//default implementation is to convert to RazorEngine encoded string
encodedString = TemplateService.EncodedStringFactory.CreateEncodedString(value);
writer.Write(encodedString);
}
}
}
}
Then you have to use HtmlTemplateBase (modified base on https://antaris.github.io/RazorEngine/TemplateBasics.html#Extending-the-template-Syntax):
var config = new TemplateServiceConfiguration();
// You can use the #inherits directive instead (this is the fallback if no #inherits is found).
config.BaseTemplateType = typeof(HtmlTemplateBase<>);
using (var service = RazorEngineService.Create(config))
{
string template = "<h2> Welcome To Page </h2> <br/> #Html.Action(\"actionName\", \"controllerName\")";
string result = service.RunCompile(template, "htmlRawTemplate", null, null);
}
in essence, it is telling the RazorEngine to use a base template where mvc is involved, so that Html and Url helper can be used.

Web API Help pages - customizing Property documentation

I have my web api and I added the web api help pages to auto-generate my documentation. It's working great for methods where my parameters are listed out, but I have a method like this:
public SessionResult PostLogin(CreateSessionCommand request)
And, on my help page, it is only listing the command parameter in the properties section. However, in the sample request section, it lists out all of the properties of my CreateSessionCommand class.
Parameters
Name | Description | Additional information
request | No documentation available. | Define this parameter in the request body.
I would like it instead to list all of the properties in my CreateSessionCommand class. Is there an easy way to do this?
So, I managed to devise a workaround for this problem, in case anyone is interested.
In HelpPageConfigurationExtensions.cs I added the following extension method:
public static void AlterApiDescription(this ApiDescription apiDescription, HttpConfiguration config)
{
var docProvider = config.Services.GetDocumentationProvider();
var addParams = new List<ApiParameterDescription>();
var removeParams = new List<ApiParameterDescription>();
foreach (var param in apiDescription.ParameterDescriptions)
{
var type = param.ParameterDescriptor.ParameterType;
//string is some special case that is not a primitive type
//also, compare by full name because the type returned does not seem to match the types generated by typeof
bool isPrimitive = type.IsPrimitive || String.Compare(type.FullName, typeof(string).FullName) == 0;
if (!isPrimitive)
{
var properties = from p in param.ParameterDescriptor.ParameterType.GetProperties()
let s = p.SetMethod
where s.IsPublic
select p;
foreach (var property in properties)
{
var documentation = docProvider.GetDocumentation(new System.Web.Http.Controllers.ReflectedHttpParameterDescriptor()
{
ActionDescriptor = param.ParameterDescriptor.ActionDescriptor,
ParameterInfo = new CustomParameterInfo(property)
});
addParams.Add(new ApiParameterDescription()
{
Documentation = documentation,
Name = property.Name,
Source = ApiParameterSource.FromBody,
ParameterDescriptor = param.ParameterDescriptor
});
}
//since this is a complex type, select it to be removed from the api description
removeParams.Add(param);
}
}
//add in our new items
foreach (var item in addParams)
{
apiDescription.ParameterDescriptions.Add(item);
}
//remove the complex types
foreach (var item in removeParams)
{
apiDescription.ParameterDescriptions.Remove(item);
}
}
And here is the Parameter info instanced class I use
internal class CustomParameterInfo : ParameterInfo
{
public CustomParameterInfo(PropertyInfo prop)
{
base.NameImpl = prop.Name;
}
}
Then, we call the extension in another method inside the extensions class
public static HelpPageApiModel GetHelpPageApiModel(this HttpConfiguration config, string apiDescriptionId)
{
object model;
string modelId = ApiModelPrefix + apiDescriptionId;
if (!config.Properties.TryGetValue(modelId, out model))
{
Collection<ApiDescription> apiDescriptions = config.Services.GetApiExplorer().ApiDescriptions;
ApiDescription apiDescription = apiDescriptions.FirstOrDefault(api => String.Equals(api.GetFriendlyId(), apiDescriptionId, StringComparison.OrdinalIgnoreCase));
if (apiDescription != null)
{
apiDescription.AlterApiDescription(config);
HelpPageSampleGenerator sampleGenerator = config.GetHelpPageSampleGenerator();
model = GenerateApiModel(apiDescription, sampleGenerator);
config.Properties.TryAdd(modelId, model);
}
}
return (HelpPageApiModel)model;
}
The comments that are used for this must be added to the controller method and not the properties of the class object. This might be because my object is part of an outside library
this should go as an addition to #Josh answer. If you want not only to list properties from the model class, but also include documentation for each property, Areas/HelpPage/XmlDocumentationProvider.cs file should be modified as follows:
public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
{
ReflectedHttpParameterDescriptor reflectedParameterDescriptor = parameterDescriptor as ReflectedHttpParameterDescriptor;
if (reflectedParameterDescriptor != null)
{
if (reflectedParameterDescriptor.ParameterInfo is CustomParameterInfo)
{
const string PropertyExpression = "/doc/members/member[#name='P:{0}']";
var pi = (CustomParameterInfo) reflectedParameterDescriptor.ParameterInfo;
string selectExpression = String.Format(CultureInfo.InvariantCulture, PropertyExpression, pi.Prop.DeclaringType.FullName + "." + pi.Prop.Name);
XPathNavigator methodNode = _documentNavigator.SelectSingleNode(selectExpression);
if (methodNode != null)
{
return methodNode.Value.Trim();
}
}
else
{
XPathNavigator methodNode = GetMethodNode(reflectedParameterDescriptor.ActionDescriptor);
if (methodNode != null)
{
string parameterName = reflectedParameterDescriptor.ParameterInfo.Name;
XPathNavigator parameterNode = methodNode.SelectSingleNode(String.Format(CultureInfo.InvariantCulture, ParameterExpression, parameterName));
if (parameterNode != null)
{
return parameterNode.Value.Trim();
}
}
}
}
return null;
}
and CustomParameterInfo class should keep property info as well:
internal class CustomParameterInfo : ParameterInfo
{
public PropertyInfo Prop { get; private set; }
public CustomParameterInfo(PropertyInfo prop)
{
Prop = prop;
base.NameImpl = prop.Name;
}
}
This is currently not supported out of the box. Following bug is kind of related to that:
http://aspnetwebstack.codeplex.com/workitem/877