I am adding documentation to the model of our API (.Net Framework 4.7.2).
I usually use something like:
''' <summary>
''' My summary
''' </summary>
''' <remarks>My remarks...</remarks>
Public Property MyProperty() As SomeClass
When I access the Model of the Swagger documentation, I see:
MyProperty (SomeClass): My summary ,
What should I do to see also "My Remarks" (maybe when I hover on the text if not immediately after the Summary)?
Thanks
Currently the <remarks> are only displayed for the controller actions, but not for the parameters.
See [Feature Request] Add remarks to parameters · Issue #1937 · domaindrivendev/Swashbuckle.AspNetCore
... the Swagger/OpenAPI Parameter object only supports a single description field for entering descriptive text. For simplicity, I'd like to maintain a 1:1 mapping between XML Comments tag and Swagger/OpenAPI field as opposed to combining/formatting multiple tags into the one field. So, your request is not something I plan on adding to SB at this point.
But I found out it could be added with custom IParameterFilter
/// <summary>
/// Add the content of <remarks> to the description of a parameter
/// Inspired by <see cref="Swashbuckle.AspNetCore.SwaggerGen.XmlCommentsParameterFilter"/>
/// </summary>
public class AddRemarksToParameterDescription : IParameterFilter
{
private readonly XPathNavigator _xmlNavigator;
public AddRemarksToParameterDescription(XPathDocument xmlDoc)
{
_xmlNavigator = xmlDoc.CreateNavigator()!;
}
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
if (context.PropertyInfo != null)
{
ApplyPropertyTags(parameter, context);
}
else if (context.ParameterInfo != null)
{
ApplyParamTags(parameter, context);
}
}
private void ApplyPropertyTags(OpenApiParameter parameter, ParameterFilterContext context)
{
var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.PropertyInfo);
var propertyNode = _xmlNavigator.SelectSingleNode($"/doc/members/member[#name='{propertyMemberName}']");
if (propertyNode == null) return;
var remarksNode = propertyNode.SelectSingleNode("remarks");
if (remarksNode != null)
{
parameter.Description += FormatRemarks(remarksNode.InnerXml);
}
}
private void ApplyParamTags(OpenApiParameter parameter, ParameterFilterContext context)
{
if (!(context.ParameterInfo.Member is MethodInfo methodInfo)) return;
// If method is from a constructed generic type, look for comments from the generic type method
var targetMethod = methodInfo.DeclaringType is { IsConstructedGenericType: true }
? methodInfo.GetUnderlyingGenericTypeMethod()
: methodInfo;
if (targetMethod == null) return;
var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod);
var paramNode = _xmlNavigator.SelectSingleNode(
$"/doc/members/member[#name='{methodMemberName}']/param[#name='{context.ParameterInfo.Name}']");
if (paramNode != null)
{
var remarksNode = paramNode.SelectSingleNode("remarks");
if (remarksNode != null)
{
parameter.Description += FormatRemarks(remarksNode.InnerXml);
}
}
}
private static string FormatRemarks(string text)
{
return "<br><br>Remarks: <br><i>" + XmlCommentsTextHelper.Humanize(text) + "</i>";
}
}
Add in your setup:
instead of this: (from the official readme)
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1",
new OpenApiInfo
{
Title = "My API - V1",
Version = "v1"
}
);
var filePath = Path.Combine(System.AppContext.BaseDirectory, "MyApi.xml");
c.IncludeXmlComments(filePath);
});
Do this:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1",
new OpenApiInfo
{
Title = "My API - V1",
Version = "v1"
}
);
var filePath = Path.Combine(System.AppContext.BaseDirectory, "MyApi.xml");
// ==== NEW
var xmlDox = new XPathDocument(filePath); // Re-use XPathDocument
c.IncludeXmlComments(() => xmlDox); // IncludeXmlComments with current XPathDocument
c.ParameterFilter<AddRemarksToParameterDescription>(xmlDox); // The new filter
});
Tested with Swashbuckle.AspNetCore 5.5 + C# 8 with nullable analysis. I think it should also work with Swashbuckle.AspNetCore 6
Related
I have this component that displays generic messages:
<span>#message</span>
The messages are identified by an id and come from string tables in resources files (multiple languages). An example of a message would be:
"Hello {user}! Welcome to {site}!"
So in the basic case, I simply parse the string and replace {user} with, say, "John Doe" and {site} with "MySiteName". The result is set to message and is then properly (and safely) rendered.
But what I would like to do is actually replace {site} with a component that I created that displays the site name with special font and styling. I also have other cases where I want to replace special {markings} with components.
How would you approach this problem ? Is there a way to "insert" a component into a string and then insert the string "safely" to be rendered ? I say "safely" because portions of the final string may come from the DB and be inherently unsafe (like user's name) so inserting the string with something like #((MarkupString)message) does not seem safe.
EDIT:
Thanks to MrC aka Shaun Curtis from whom this final solution is greatly inspired. I marked his answer as the best one.
So I finally went with a scoped service that gets the strings from the resources files, parse them and return a list of RenderFragments that it gets from a component's static table. I use dynamic objects to send specific parameters to the RenderFragments when required.
I basically now get all the text of my app through this centralized mechanism.
Here is an example of an entry in a resource file string table:
Name: "welcome"; Value: "Welcome to {site:name} {0}!"
Here is how it is used in a component:
<h3><Localizer Key="notif:welcome" Data="#(new List<string>() { NotifModel.UserNames.First })"/></h3>
You can see the simplified component and service code below. I explicitely left out the validation and error checking code for simplicity.
#using MySite.Client.Services.Localizer
#inject ILocalizerService Loc
#foreach (var fragment in _fragments)
{
#fragment.Renderer(fragment.Item)
}
#code
{
private List<ILocalizerService.Fragment> _fragments;
public enum RendererTypes
{
Default,
SiteName,
SiteLink,
}
public static Dictionary<RendererTypes, RenderFragment<dynamic>> Renderers = new Dictionary<RendererTypes, RenderFragment<dynamic>>()
{
// NOTE: For each of the following items, do NOT insert a space between the end of the markup and the closing curly brace otherwise it will be rendered !!!
// Like here ↓↓
{ RendererTypes.Default, (model) => #<span>#(model as string)</span>},
{ RendererTypes.SiteName, (model) => #<MySiteNameComponent />},
{ RendererTypes.SiteLink, (model) => ##model.LinkTxt}
};
[Parameter]
public string Key { get; set; }
[Parameter]
public List<string> Data { get; set; }
protected override void OnParametersSet()
{
_fragments = Loc.GetFragments(Key, Data);
}
}
interface ILocalizerService
{
public struct Fragment
{
public Fragment(RenderFragment<dynamic> renderer)
: this(renderer, default)
{
}
public Fragment(RenderFragment<dynamic> renderer, dynamic item)
{
Renderer = renderer;
Item = item;
}
public RenderFragment<dynamic> Renderer { get; set; }
public dynamic Item { get; set; }
}
List<Fragment> GetFragments(string key, List<string> parameters);
}
internal sealed class LocalizerService : ILocalizerService
{
private readonly Dictionary<string, IStringLocalizer> _strLoc = new Dictionary<string, IStringLocalizer>();
public LocalizerService(IStringLocalizer<MySite.Shared.Resources.App> appLoc,
IStringLocalizer<MySite.Shared.Resources.Connection> connLoc,
IStringLocalizer<MySite.Shared.Resources.Notifications> notifLoc)
{
// Keep string localizers
_strLoc.Add("app", appLoc);
_strLoc.Add("conn", connLoc);
_strLoc.Add("notif", notifLoc);
}
public List<Fragment> GetFragments(string key, List<string> parameters)
{
var list = new List<Fragment>();
GetFragments(list, key, parameters);
return list;
}
private void GetFragments(List<Fragment> list, string key, List<string> parameters)
{
// First, get key tokens
var tokens = key.Split(':');
// Analyze first token
switch (tokens[0])
{
case "site":
// Format : {site:...}
ProcessSite(list, tokens, parameters);
break;
default:
// Format : {0|1|2|...}
if (uint.TryParse(tokens[0], out var paramIndex))
{
ProcessParam(list, paramIndex, parameters);
}
// Format : {app|conn|notif|...}
else if (_strLoc.ContainsKey(tokens[0]))
{
ProcessStringLocalizer(list, tokens, parameters);
}
break;
}
}
private void ProcessSite(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Analyze second token
switch (tokens[1])
{
case "name":
// Format {site:name}
// Add name component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteName]));
break;
case "link":
// Format {site:link:...}
ProcessLink(list, tokens, parameters);
break;
}
}
private void ProcessLink(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Analyze third token
switch (tokens[2])
{
case "user":
// Format: {site:link:user:...}
ProcessLinkUser(list, tokens, parameters);
break;
}
}
private void ProcessLinkUser(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Check length
var length = tokens.Length;
if (length >= 4)
{
string linkUrl;
string linkTxt;
// URL
// Format: {site:link:user:0|1|2|...}
// Retrieve handle from param
if (!uint.TryParse(tokens[3], out var paramIndex))
{
throw new ApplicationException("Invalid token!");
}
var userHandle = GetParam(paramIndex, parameters);
linkUrl = $"/user/{userHandle}";
// Text
if (length >= 5)
{
if (tokens[4].Equals("t"))
{
// Format: {site:link:user:0|1|2|...:t}
// Use token directly as text
linkTxt = tokens[4];
}
else if (uint.TryParse(tokens[4], out paramIndex))
{
// Format: {site:link:user:0|1|2|...:0|1|2|...}
// Use specified param as text
linkTxt = GetParam(paramIndex, parameters);
}
}
else
{
// Format: {site:link:user:0|1|2|...}
// Use handle as text
linkTxt = userHandle;
}
// Add link component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.SiteLink], new { LinkUrl = linkUrl, LinkTxt = linkTxt }));
}
}
private void ProcessParam(List<Fragment> list, uint paramIndex, List<string> parameters)
{
// Add text component
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], GetParam(paramIndex, parameters)));
}
private string GetParam(uint paramIndex, List<string> parameters)
{
// Proceed
if (paramIndex < parameters.Length)
{
return parameters[paramIndex];
}
}
private void ProcessStringLocalizer(List<Fragment> list, string[] tokens, List<string> parameters)
{
// Format {loc:str}
// Retrieve string localizer
var strLoc = _strLoc[tokens[0]];
// Retrieve string
var str = strLoc[tokens[1]].Value;
// Split the string in parts to see if it needs formatting
// NOTE: str is in the form "...xxx {key0} yyy {key1} zzz...".
// This means that once split, the keys are always at odd indexes (even if {key} starts or ends the string)
var strParts = str.Split('{', '}');
for (int i = 0; i < strParts.Length; i += 2)
{
// Get parts
var evenPart = strParts[i];
var oddPart = ((i + 1) < strParts.Length) ? strParts[i + 1] : null;
// Even parts are always regular text. If not null or empty, we add directly
if (!string.IsNullOrEmpty(evenPart))
{
list.Add(new Fragment(Shared.Localizer.Renderers[Shared.Localizer.RendererTypes.Default], evenPart));
}
// Odd parts are always keys. If not null or empty, get fragments recursively
if (!string.IsNullOrEmpty(oddPart))
{
GetFragments(list, oddPart, parameters);
}
}
}
}
You don't necessarily need to build components. A component is a c# class that emits a RenderFragment.
You could simply build RenderFragments for {site},... Here's a simple static class that shows two ways to do this:
namespace StackOverflowAnswers;
public static class RenderFragements
{
public static RenderFragment SiteName => (builder) =>
{
// Get the content from a service that's accessing a database and checking the culture info for language
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "p-2 bg-primary text-white");
builder.AddContent(2, "My Site");
builder.CloseElement();
};
public static RenderFragment GetSiteName(string sitename) => (builder) =>
{
// parse to make sure you're happy with the string
builder.OpenElement(0, "span");
builder.AddAttribute(1, "class", "p-2 bg-dark text-white");
builder.AddContent(2, sitename);
builder.CloseElement();
};
}
And here's an index page using them:
#page "/"
#using StackOverflowAnswers
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class=m-2>
The site name for this site is #(RenderFragements.GetSiteName("this site"))
</div>
#(RenderFragements.SiteName)
With the RenderFragment your writing c# code. You can run a parser to check the string before rendering it.
You could have a scoped service that gets the info from the database for the user and exposes a set of RenderFragments you then use in your pages/components.
I used regex to split the source at the tokens configured in TokenMappings. Token mappings could easily be loaded from a json source for example. To configure more "{markings}" just add more lines to the TokenMappings.
<StringParser Source="Hello {user}! Welcome to {site}!" />
StringParser.razor
#foreach (var subString in substrings)
{
if (tokens.Contains(subString))
{
var key = StripCurlyBrackets(subString);
<DynamicComponent Type=#(TokenMappings[key].Item1)
Parameters=#(TokenMappings[key].Item2) />
}
else
{
#subString
}
}
#code {
private Dictionary<string, (Type, Dictionary<string, object>?)> TokenMappings;
private string[] substrings;
private string[] tokens;
[Parameter]
public string Source { get; set; }
protected override void OnParametersSet()
{
var user = "John Doe"; // I would expect these are supplied via a signin context.
var site = "MySiteName"; //
TokenMappings = new Dictionary<string, (Type, Dictionary<string, object>?)>
{
{ "user", ( typeof(UserComponent), new Dictionary<string, object>{ { "User", user } } ) },
{ "site", ( typeof(SiteComponent), new Dictionary<string, object>{ { "Site", site } } ) }
};
var keys = TokenMappings.Keys.Select(a => a);
var pattern = keys.Select(key => $"({{(?:{key})}})").Aggregate((a, b) => a + "|" + b);
this.substrings = System.Text.RegularExpressions.Regex.Split(Source, pattern);
this.tokens = TokenMappings!.Keys.Select(key => $"{{{key}}}").ToArray();
base.OnParametersSet();
}
private string StripCurlyBrackets(string source)
{
return source
.Replace(oldValue: "{", newValue: string.Empty)
.Replace(oldValue: "}", newValue: string.Empty);
}
}
Yes MarkupString allows you to render html.
substrings :
I got an MVC 5 application that i'm porting to asp.net Core.
In the MVC application call to controller we're made using AngularJS $resource (sending JSON) and we we're POSTing data doing :
ressource.save({ entries: vm.entries, projectId: vm.project.id }).$promise...
that will send a JSON body like:
{
entries:
[
{
// lots of fields
}
],
projectId:12
}
the MVC controller looked like this :
[HttpPost]
public JsonResult Save(List<EntryViewModel> entries, int projectId) {
// code here
}
How can I replicate the same behaviour with .NET Core since we can't have multiple [FromBody]
you cannot have multiple parameter with the FromBody attibute in an action method. If that is need, use a complex type such as a class with properties equivalent to the parameter or dynamic type like that
[HttpPost("save/{projectId}")]
public JsonResult Save(int projectId, [FromBody] dynamic entries) {
// code here
}
As pointed out in the comment, one possible solution is to unify the properties you're posting onto a single model class.
Something like the following should do the trick:
public class SaveModel
{
public List<EntryViewModel> Entries{get;set;}
public int ProjectId {get;set;}
}
Don't forget to decorate the model with the [FromBody] attribute:
[HttpPost]
public JsonResult Save([FromBody]SaveViewModel model)
{
// code here
}
Hope this helps!
It's still rough but I made a Filter to mimic the feature.
public class OldMVCFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.Method != "GET")
{
var body = context.HttpContext.Request.Body;
JToken token = null;
var param = context.ActionDescriptor.Parameters;
using (var reader = new StreamReader(body))
using (var jsonReader = new JsonTextReader(reader))
{
jsonReader.CloseInput = false;
token = JToken.Load(jsonReader);
}
if (token != null)
{
var serializer = new JsonSerializer();
serializer.DefaultValueHandling = DefaultValueHandling.Populate;
serializer.FloatFormatHandling = FloatFormatHandling.DefaultValue;
foreach (var item in param)
{
JToken model = token[item.Name];
if (model == null)
{
// try to cast the full body as the current object
model = token.Root;
}
if (model != null)
{
model = this.RemoveEmptyChildren(model, item.ParameterType);
var res = model.ToObject(item.ParameterType, serializer);
context.ActionArguments[item.Name] = res;
}
}
}
}
}
private JToken RemoveEmptyChildren(JToken token, Type type)
{
var HasBaseType = type.GenericTypeArguments.Count() > 0;
List<PropertyInfo> PIList = new List<PropertyInfo>();
if (HasBaseType)
{
PIList.AddRange(type.GenericTypeArguments.FirstOrDefault().GetProperties().ToList());
}
else
{
PIList.AddRange(type.GetTypeInfo().GetProperties().ToList());
}
if (token != null)
{
if (token.Type == JTokenType.Object)
{
JObject copy = new JObject();
foreach (JProperty jProp in token.Children<JProperty>())
{
var pi = PIList.FirstOrDefault(p => p.Name == jProp.Name);
if (pi != null) // If destination type dont have this property we ignore it
{
JToken child = jProp.Value;
if (child.HasValues)
{
child = RemoveEmptyChildren(child, pi.PropertyType);
}
if (!IsEmpty(child))
{
if (child.Type == JTokenType.Object || child.Type == JTokenType.Array)
{
// nested value has been checked, we add the object
copy.Add(jProp.Name, child);
}
else
{
if (!pi.Name.ToLowerInvariant().Contains("string"))
{
// ignore empty value when type is not string
var Val = (string)child;
if (!string.IsNullOrWhiteSpace(Val))
{
// we add the property only if it contain meningfull data
copy.Add(jProp.Name, child);
}
}
}
}
}
}
return copy;
}
else if (token.Type == JTokenType.Array)
{
JArray copy = new JArray();
foreach (JToken item in token.Children())
{
JToken child = item;
if (child.HasValues)
{
child = RemoveEmptyChildren(child, type);
}
if (!IsEmpty(child))
{
copy.Add(child);
}
}
return copy;
}
return token;
}
return null;
}
private bool IsEmpty(JToken token)
{
return (token.Type == JTokenType.Null || token.Type == JTokenType.Undefined);
}
}
I am following the sample blog below to remove and add properties in request ODataEntry class.
http://blogs.msdn.com/b/odatateam/archive/2013/07/26/using-the-new-client-hooks-in-wcf-data-services-client.aspx
But even if the code works fine and adds and removes the properties correctly when I put breakpoint, all the entity properties goes to server un changed.
Only difference I see this I am using the OData V4 and new Ondata client to hook up.
My code looks below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Client.Default;
namespace Client
{
using Client.MvcApplication1.Models;
using Microsoft.OData.Core;
internal class Program
{
private static void Main(string[] args)
{
Container container = new Container(new Uri("http://localhost:55000/api/"));
container.Configurations.RequestPipeline.OnEntryEnding(
w =>
{
w.Entry.RemoveProperties("Name");
});
Test test = new Test();
test.Name = "Foo";
CustomFields cs = new CustomFields { ServiceId = 3 };
cs.Foo1 = 2;
test.S_1 = cs;
container.AddToTests(test);
container.SaveChanges();
}
}
public static class Extensions
{
public static void RemoveProperties(this ODataEntry entry, params string[] propertyNames)
{
var properties = entry.Properties as List<ODataProperty>;
if (properties == null)
{
properties = new List<ODataProperty>(entry.Properties);
}
var propertiesToRemove = properties.Where(p => propertyNames.Any(rp => rp == p.Name));
foreach (var propertyToRemove in propertiesToRemove.ToArray())
{
properties.Remove(propertyToRemove);
}
entry.Properties = properties;
}
public static void AddProperties(this ODataEntry entry, params ODataProperty[] newProperties)
{
var properties = entry.Properties as List<ODataProperty>;
if (properties == null)
{
properties = new List<ODataProperty>(entry.Properties);
}
properties.AddRange(newProperties);
entry.Properties = properties;
}
}
}
If I change and start listening to RequestPipeline.OnEntryStarting I get the validation error that new property is not defined in owning entity. But as per code for Microsoft.OData.CLient this error should not occure as there is a check for IEdmStructuredType.IsOpen but still error occurs. So issue seems deep in how owningStructuredType is calculated. On my container I do see the correct edm model with entities marked as IsOpen = true.
Odata lib code which should pass but is failing
internal static IEdmProperty ValidatePropertyDefined(string propertyName, IEdmStructuredType owningStructuredType)
{
Debug.Assert(!string.IsNullOrEmpty(propertyName), "!string.IsNullOrEmpty(propertyName)");
if (owningStructuredType == null)
{
return null;
}
IEdmProperty property = owningStructuredType.FindProperty(propertyName);
// verify that the property is declared if the type is not an open type.
if (!owningStructuredType.IsOpen && property == null)
{
throw new ODataException(Strings.ValidationUtils_PropertyDoesNotExistOnType(propertyName, owningStructuredType.ODataFullName()));
}
return property;
}
Client code:
container.Configurations.RequestPipeline.OnEntryStarting(
w =>
{
w.Entry.RemoveProperties("Name");
w.Entry.AddProperties(new ODataProperty
{
Name = "NewProperty",
Value = 1
});
});
Error:
The property 'NewProperty' does not exist on type 'Client.MvcApplication1.Models.Test'. Make sure to only use property names that are defined by the type.
at Microsoft.OData.Core.WriterValidationUtils.ValidatePropertyDefined(String propertyName, IEdmStructuredType owningStructuredType)
at Microsoft.OData.Core.JsonLight.ODataJsonLightPropertySerializer.WriteProperty(ODataProperty property, IEdmStructuredType owningType, Boolean isTopLevel, Boolean allowStreamProperty, DuplicatePropertyNamesChecker duplicatePropertyNamesChecker, ProjectedPropertiesAnnotation projectedProperties)
at Microsoft.OData.Core.JsonLight.ODataJsonLightPropertySerializer.WriteProperties(IEdmStructuredType owningType, IEnumerable`1 properties, Boolean isComplexValue, DuplicatePropertyNamesChecker duplicatePropertyNamesChecker, ProjectedPropertiesAnnotation projectedProperties)
at Microsoft.OData.Core.JsonLight.ODataJsonLightWriter.StartEntry(ODataEntry entry)
at Microsoft.OData.Core.ODataWriterCore.<>c__DisplayClass14.<WriteStartEntryImplementation>b__12()
at Microsoft.OData.Core.ODataWriterCore.InterceptException(Action action)
at Microsoft.OData.Core.ODataWriterCore.WriteStartEntryImplementation(ODataEntry entry)
at Microsoft.OData.Core.ODataWriterCore.WriteStart(ODataEntry entry)
at Microsoft.OData.Client.ODataWriterWrapper.WriteStart(ODataEntry entry, Object entity)
at Microsoft.OData.Client.Serializer.WriteEntry(EntityDescriptor entityDescriptor, IEnumerable`1 relatedLinks, ODataRequestMessageWrapper requestMessage)
at Microsoft.OData.Client.BaseSaveResult.CreateRequestData(EntityDescriptor entityDescriptor, ODataRequestMessageWrapper requestMessage)
at Microsoft.OData.Client.BaseSaveResult.CreateChangeData(Int32 index, ODataRequestMessageWrapper requestMessage)
at Microsoft.OData.Client.SaveResult.CreateNonBatchChangeData(Int32 index, ODataRequestMessageWrapper requestMessage)
at Microsoft.OData.Client.SaveResult.CreateNextChange()
I use partial classes defined on the client to add the extra properties that I need there. This allows me to put any logic in them as well as have property changed notification as well. I the use the following extension methods to remove those properties. I think I actually got the original code from the article that you linked.
public static class DbContextExtensions
{
public static void RemoveProperties(this ODataEntry entry, params string[] propertyNames)
{
var properties = entry.Properties as List<ODataProperty>;
if (properties == null)
{
properties = new List<ODataProperty>(entry.Properties);
}
var propertiesToRemove = properties.Where(p => propertyNames.Any(rp => rp == p.Name));
foreach (var propertyToRemove in propertiesToRemove.ToArray())
{
properties.Remove(propertyToRemove);
}
entry.Properties = properties;
}
public static DataServiceClientResponsePipelineConfiguration RemoveProperties<T>(this DataServiceClientResponsePipelineConfiguration responsePipeline, Func<string, Type> resolveType, params string[] propertiesToRemove)
{
return responsePipeline.OnEntryEnded((args) =>
{
Type resolvedType = resolveType(args.Entry.TypeName);
if (resolvedType != null && typeof(T).IsAssignableFrom(resolvedType))
{
args.Entry.RemoveProperties(propertiesToRemove);
}
});
}
public static DataServiceClientRequestPipelineConfiguration RemoveProperties<T>(this DataServiceClientRequestPipelineConfiguration requestPipeline, params string[] propertiesToRemove)
{
return requestPipeline.OnEntryStarting((args) =>
{
if (typeof(T).IsAssignableFrom(args.Entity.GetType()))
{
args.Entry.RemoveProperties(propertiesToRemove);
}
});
}
}
Notice that in the method below it is hooking OnEntryStarted. The code in the article hooks OnEntryEnded which worked for me at one point and then broke when I updated to a newer version of ODataClient. OnEntryStarted is the way to go in this method.
public static DataServiceClientRequestPipelineConfiguration RemoveProperties<T>(this DataServiceClientRequestPipelineConfiguration requestPipeline, params string[] propertiesToRemove)
{
return requestPipeline.OnEntryStarting((args) =>
{
if (typeof(T).IsAssignableFrom(args.Entity.GetType()))
{
args.Entry.RemoveProperties(propertiesToRemove);
}
});
}
I also created a partial class for the Container as well and implement the partial method OnContextCreated. This is where you use the extension methods to remove the properties that won't get sent to the server.
partial void OnContextCreated()
{
Configurations.RequestPipeline.RemoveProperties<Customer>(new string[] { "FullName", "VersionDetails" });
Configurations.RequestPipeline.RemoveProperties<SomeOtherType>(new string[] { "IsChecked", "IsReady" });
}
Make sure that your partial classes and the DBContextExtensions class are in the same namespace as our container and everything should just work.
Hope that helps.
I'm stuck. I have an existing application with an extremely large database and extensive library of stored procedures and functions. All I want to do is use a DbContext to execute a stored procedure and return a set of data or map to one of the entities in the context. Is that something magical I haven't discovered on the net somewhere? Someone, anyone, please help. Here's what I've got so far (and it doesn't return anything, the result is -1):
var contacts = db.Database.ExecuteSqlCommand("Contact_Search #LastName, #FirstName",
new SqlParameter("#LastName", GetDataValue(args.LastName)),
new SqlParameter("#FirstName", GetDataValue(args.FirstName)));
Executing that returns -1. I also tried something to the effect of this with no success:
DbRawSqlQuery<Contact> data = db.Database.SqlQuery<Contact>
("EXEC Contact_Search #LastName, #FirstName",
GetDataValue(args.LastName),
GetDataValue(args.FirstName));
I understand that I could add an edmx and map to a stored procedure that way, but that is not the preferred method. Again, our database contains nearly 450 million records and a library of almost 3,000 stored procedures and functions. It would be a nightmare to maintain. Am I even starting in the right direction? Is Entity Framework the right choice?
Wow, it seems right after I give up, I somehow stumble upon the answer. I found a FANTASTIC post about executing stored procedures and after reading up, this was my solution:
var contacts = db.Database.SqlQuery<Contact>("Contact_Search #LastName, #FirstName",
So, many thanks to Anuraj for his excellent post! The key to my solution was to first use SqlQuery instead of ExecuteSqlCommand, and also to execute the method mapping to my entity model (Contact).
This code is better than SqlQuery() because SqlQuery() doesn't recognise the [Column] attribute.
Here it is on a silver platter.
public static class StoredProcedureExtensions {
/// <summary>
/// Execute Stored Procedure and return result in an enumerable object.
/// </summary>
/// <typeparam name="TEntity">Type of enumerable object class to return.</typeparam>
/// <param name="commandText">SQL query.</param>
/// <param name="parameters">SQL parameters.</param>
/// <param name="readOnly">Determines whether to attach and track changes for saving. Defaults to true and entities will not be tracked and thus a faster call.</param>
/// <returns>IEnumerable of entity type.</returns>
public static IEnumerable<TEntity> GetStoredProcedureResults<TEntity>(this DbContext dbContext, string query, Dictionary<string, object> parameters, bool readOnly = true) where TEntity : class, new()
{
SqlParameter[] sqlParameterArray = DbContextExtensions.DictionaryToSqlParameters(parameters);
return dbContext.GetStoredProcedureResults<TEntity>(query, sqlParameterArray, readOnly);
}
/// <summary>
/// Execute Stored Procedure and return result in an enumerable object.
/// </summary>
/// <typeparam name="TEntity">Type of enumerable object class to return.</typeparam>
/// <param name="commandText">SQL query.</param>
/// <param name="parameters">SQL parameters.</param>
/// <param name="readOnly">Determines whether to attach and track changes for saving. Defaults to true and entities will not be tracked and thus a faster call.</param>
/// <returns>IEnumerable of entity type.</returns>
public static IEnumerable<TEntity> GetStoredProcedureResults<TEntity>(this DbContext dbContext, string commandText, SqlParameter[] sqlParameterArray = null, bool readOnly = true) where TEntity : class, new()
{
string infoMsg = commandText;
try
{
//---- For a better error message
if (sqlParameterArray != null)
{
foreach (SqlParameter p in sqlParameterArray)
{
infoMsg += string.Format(" {0}={1}, ", p.ParameterName, p.Value == null ? "(null)" : p.Value.ToString());
}
infoMsg = infoMsg.Trim().TrimEnd(',');
}
///////////////////////////
var reader = GetReader(dbContext, commandText, sqlParameterArray, CommandType.StoredProcedure);
///////////////////////////
///////////////////////////
List<TEntity> results = GetListFromDataReader<TEntity>(reader);
///////////////////////////
if(readOnly == false)
{
DbSet entitySet = dbContext.Set<TEntity>(); // For attaching the entities so EF can track changes
results.ForEach(n => entitySet.Attach(n)); // Add tracking to each entity
}
reader.Close();
return results.AsEnumerable();
}
catch (Exception ex)
{
throw new Exception("An error occurred while executing GetStoredProcedureResults(). " + infoMsg + ". Check the inner exception for more details.\r\n" + ex.Message, ex);
}
}
//========================================= Private methods
#region Private Methods
private static DbDataReader GetReader(DbContext dbContext, string commandText, SqlParameter[] sqlParameterArray, CommandType commandType)
{
var command = dbContext.Database.Connection.CreateCommand();
command.CommandText = commandText;
command.CommandType = commandType;
if (sqlParameterArray != null) command.Parameters.AddRange(sqlParameterArray);
dbContext.Database.Connection.Open();
var reader = command.ExecuteReader(CommandBehavior.CloseConnection);
return reader;
}
private static List<TEntity> GetListFromDataReader<TEntity>(DbDataReader reader) where TEntity : class, new()
{
PropertyInfo[] entityProperties = typeof(TEntity).GetProperties();
IEnumerable<string> readerColumnNames = (reader.GetSchemaTable().Select()).Select(r => r.ItemArray[0].ToString().ToUpper()); // uppercase reader column names.
List<MappingPropertyToColumn> propertyToColumnMappings = GetPropertyToColumnMappings<TEntity>(); // Maps the entity property names to the corresponding names of the columns in the reader
var entityList = new List<TEntity>(); // Fill this
while (reader.Read())
{
var element = Activator.CreateInstance<TEntity>();
foreach (var entityProperty in entityProperties)
{
MappingPropertyToColumn mapping = propertyToColumnMappings._Find(entityProperty.Name);
if (mapping == null) // This property has a [Not Mapped] attribute
{
continue; // Skip this one
}
var o = (object)reader[mapping.ColumnName]; // mapping must match all mapped properties to columns. If result set does not contain a column, then throw error like EF would.
bool hasValue = o.GetType() != typeof(DBNull);
if (mapping.IsEnum && hasValue) // Enum
{
entityProperty.SetValue(element, Enum.Parse(mapping.UnderlyingType, o.ToString()));
}
else
{
if (hasValue)
{
entityProperty.SetValue(element, ChangeType(o, entityProperty.PropertyType));
}
}
}
entityList.Add(element);
}
return entityList;
}
public static object ChangeType(object value, Type conversion)
{
var t = conversion;
if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
{
if (value == null)
{
return null;
}
t = Nullable.GetUnderlyingType(t);
}
return Convert.ChangeType(value, t);
}
private static List<MappingPropertyToColumn> GetPropertyToColumnMappings<TEntity>() where TEntity : new()
{
var type = typeof(TEntity);
List<MappingPropertyToColumn> databaseMappings = new List<MappingPropertyToColumn>();
foreach (var entityProperty in type.GetProperties())
{
bool isEnum = entityProperty.PropertyType.IsEnum;
// [Not Mapped] Not Mapped Attribute
var notMapped = entityProperty.GetCustomAttributes(false).FirstOrDefault(attribute => attribute is NotMappedAttribute);
if (notMapped != null) // This property has a [Not Mapped] attribute
{
continue; // Skip this property
}
// Determine if property is an enum
Type underlyingType = null;
if (entityProperty.PropertyType.IsGenericType && entityProperty.PropertyType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
{
underlyingType = Nullable.GetUnderlyingType(entityProperty.PropertyType); ;
if (underlyingType != null && underlyingType.IsEnum)
{
isEnum = true;
}
}
// [Column("tbl_columnname")] Column Name Attribute for mapping
var columnMapping = entityProperty.GetCustomAttributes(false).FirstOrDefault(attribute => attribute is ColumnAttribute);
if (columnMapping != null)
{
databaseMappings.Add(new MappingPropertyToColumn { PropertyName = entityProperty.Name, ColumnName = ((ColumnAttribute)columnMapping).Name.ToUpper(), IsEnum = isEnum, UnderlyingType = underlyingType }); // SQL case insensitive
}
else
{
databaseMappings._AddProperty(entityProperty.Name, entityProperty.Name, isEnum); // C# case sensitive
}
}
return databaseMappings;
}
//====================================== Class for holding column mappings and other info for each property
private class MappingPropertyToColumn
{
private string _propertyName;
public string PropertyName
{
get { return _propertyName; }
set { _propertyName = value; }
}
private string _columnName;
public string ColumnName
{
get { return _columnName; }
set { _columnName = value; }
}
private bool _isNullableEnum;
public bool IsEnum
{
get { return _isNullableEnum; }
set { _isNullableEnum = value; }
}
private Type _underlyingType;
public Type UnderlyingType
{
get { return _underlyingType; }
set { _underlyingType = value; }
}
}
//======================================================= List<MappingPropertyToColumn> Extension methods
#region List<MappingPropertyToColumn> Extension methods
private static bool _ContainsKey<T>(this List<T> list, string key) where T : MappingPropertyToColumn
{
return list.Any(x => x.PropertyName == key);
}
private static MappingPropertyToColumn _Find<T>(this List<T> list, string key) where T : MappingPropertyToColumn
{
return list.Where(x => x.PropertyName == key).FirstOrDefault();
}
private static void _AddProperty<T>(this List<T> list, string propertyName, string columnName, bool isEnum, Type underlyingType = null) where T : MappingPropertyToColumn
{
list.Add((T)new MappingPropertyToColumn { PropertyName = propertyName, ColumnName = columnName, IsEnum = isEnum, UnderlyingType = underlyingType }); // C# case sensitive
}
#endregion
#endregion }
In a ASP.NET MVC project I'm working on I have the following piece of code that basically inject instances to specific methods within my assemblies.
So in the application root I have a class that register the instances like this and finally handles the injection.
ApplicationServiceProvider serviceProvider = ApplicationServiceProvider.CreateDefaultProvider();
serviceProvider.RegisterInstance(GlobalConfiguration.Configuration);
serviceProvider.RegisterInstance(GlobalFilters.Filters);
serviceProvider.RegisterInstance(RouteTable.Routes);
serviceProvider.RegisterInstance(BundleTable.Bundles);
serviceProvider.Distribute();
Now when I want to access these instances from the assemblies, I have to create some handler (method) and mark it with the following attribute 'ApplicationServiceHandler' like in the following example.
[ContractVerification(false)]
public static class RouteConfiguration
{
[ApplicationServiceHandler]
public static void Register(RouteCollection routes)
{
}
}
This is part of the extensibility layer in the project which is currently working pretty good.
Now, I'm new to Autofac and I wonder whether I can use Autofac to do the work for me rather than using my own implementation (which I provided below) that probably does it less efficient and handles less cases that are already covered by Autofac.
I noticed Autofac have a RegisterInstance method but I'm not sure how to tell it to inject the instances to methods flagged with 'ApplicationServiceHandler' attribute, I'm not not sure it's even the correct method but based on the name it seems like the right one.
Any kind of help is greatly appreciated, thank you.
EDIT: Here is the code that I'm using to achieve this without Autofac in my project.
ApplicationServiceHandlerAttribute.cs
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class ApplicationServiceHandlerAttribute : Attribute
{
}
ApplicationServiceHandler.cs
public sealed class ApplicationServiceHandler
{
private readonly MethodInfo _method;
private readonly object[] _args;
public ApplicationServiceHandler(MethodInfo method, object[] args)
{
Contract.Requires(method != null);
Contract.Requires(args != null);
_method = method;
_args = args;
}
public void Invoke()
{
_method.Invoke(null, _args);
}
[ContractInvariantMethod]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
private void ObjectInvariant()
{
Contract.Invariant(_method != null);
Contract.Invariant(_args != null);
}
}
ApplicationServiceProvider.cs
public sealed class ApplicationServiceProvider
{
private readonly IEnumerable<Assembly> _assemblies;
private readonly Dictionary<Type, object> _instances;
public ApplicationServiceProvider(IEnumerable<Assembly> assemblies)
{
Contract.Requires(assemblies != null);
_assemblies = assemblies;
_instances = new Dictionary<Type, object>();
}
public static ApplicationServiceProvider CreateDefaultProvider()
{
Contract.Ensures(Contract.Result<ApplicationServiceProvider>() != null);
return new ApplicationServiceProvider(PackageLoader.ReferencedAssemblies);
}
public void Distribute()
{
foreach (var handler in GetHandlers())
{
Contract.Assume(handler != null);
handler.Invoke();
}
}
public IEnumerable<ApplicationServiceHandler> GetHandlers()
{
Contract.Ensures(Contract.Result<IEnumerable<ApplicationServiceHandler>>() != null);
if (_instances.Count == 0)
{
yield break;
}
foreach (var asm in _assemblies)
{
IEnumerable<MethodInfo> methods = GetMethods(asm);
foreach (var method in methods)
{
ParameterInfo[] #params = method.GetParameters();
if (#params.Length > 0)
{
int instanceCount = 0;
object[] args = new object[#params.Length];
for (int i = 0; i < #params.Length; i++)
{
ParameterInfo param = #params[i];
var instance = GetInstance(param);
if (instance != null)
{
instanceCount++;
args[i] = instance;
}
}
if (instanceCount > 0)
{
yield return new ApplicationServiceHandler(method, args);
}
}
}
}
}
public bool RegisterInstance(object instance)
{
Contract.Requires(instance != null);
return AddInstance(instance);
}
private static ApplicationServiceHandlerAttribute GetApplicationServiceHandlerAttribute(MethodInfo method)
{
ApplicationServiceHandlerAttribute attribute = null;
try
{
attribute = method.GetCustomAttribute<ApplicationServiceHandlerAttribute>(false);
}
catch (TypeLoadException)
{
// We don't need to do anything here for now.
}
return attribute;
}
private static IEnumerable<Type> GetDefinedTypes(Assembly assembly)
{
Contract.Requires(assembly != null);
Contract.Ensures(Contract.Result<IEnumerable<Type>>() != null);
try
{
return assembly.DefinedTypes;
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(type => type != null);
}
}
/// <summary>
/// Gets the methods that are marked with <see cref="ApplicationServiceHandlerAttribute"/> from the assembly.
/// </summary>
/// <remarks>
/// Eyal Shilony, 21/11/2012.
/// </remarks>
/// <param name="assembly">
/// The assembly.
/// </param>
/// <returns>
/// The methods that are marked with <see cref="ApplicationServiceHandlerAttribute"/> from the assembly.
/// </returns>
private static IEnumerable<MethodInfo> GetMethods(Assembly assembly)
{
Contract.Requires(assembly != null);
Contract.Ensures(Contract.Result<IEnumerable<MethodInfo>>() != null);
const TypeAttributes STATIC_TYPE_ATTRIBUTES = TypeAttributes.AutoLayout | TypeAttributes.AnsiClass | TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit;
var methods = (from type in GetDefinedTypes(assembly)
where type.Attributes == STATIC_TYPE_ATTRIBUTES
from method in type.GetMethods().AsParallel()
where GetApplicationServiceHandlerAttribute(method) != null
select method).ToArray();
return methods;
}
private bool AddInstance(object instance)
{
Type type = instance.GetType();
return AddInstance(type, instance);
}
private bool AddInstance(Type type, object instance)
{
if (!_instances.ContainsKey(type))
{
_instances.Add(type, instance);
return true;
}
return false;
}
private object GetInstance(ParameterInfo param)
{
object instance = null;
Type paramType = param.ParameterType;
if (_instances.ContainsKey(paramType))
{
instance = _instances[paramType];
}
else
{
foreach (var type in _instances.Keys.Where(type => type.IsSubclassOf(paramType)))
{
instance = _instances[type];
break;
}
}
return instance;
}
}
i hope , i have understood you correctly.if what you mean is marking a class as dependency with attributes then you can do it by creating custom attribute.following is an example of implementing such an attribute :
public class DependencyAttribute : Attribute
{
public DependencyAttribute()
{
}
//The type of service the attributed class represents
public Type ServiceType { get; set; }
//Optional key to associate with the service
public string Key { get; set; }
public virtual void RegisterService(AttributeInfo<DependencyAttribute> attributeInfo, IContainer container)
{
Type serviceType = attributeInfo.Attribute.ServiceType ?? attributeInfo.DecoratedType;
Containerbuilder builder = new ContainerBuilder();
builder.RegisterType(attributeInfo.DecoratedType).As(serviceType).Keyed(
attributeInfo.Attribute.Key ?? attributeInfo.DecoratedType.FullName);
builder.Update(container)
}
}
then you must find all types marked with this attribute and call the RegisterService method of these attributes.
public class DependencyAttributeRegistrator
{
public DependencyAttributeRegistrator()
{
}
public IEnumerable<AttributeInfo<DependencyAttribute>> FindServices()
{
//replace this line with you'r own
var types = Assembly.GetExecutingAssembly().GetTypes();
foreach (Type type in types)
{
var attributes = type.GetCustomAttributes(typeof(DependencyAttribute), false);
foreach (DependencyAttribute attribute in attributes)
{
yield return new AttributeInfo<DependencyAttribute> { Attribute = attribute, DecoratedType = type };
}
}
}
public void RegisterServices(IEnumerable<AttributeInfo<DependencyAttribute>> services)
{
foreach (var info in services)
{
//replace following two line with you'r own global container
var builder = new ContainerBuilder();
IContainer container = builder.Build();
info.Attribute.RegisterService(info, container);
}
}
}