Swagger/Redoc <remarks> not showing - asp.net

I am trying to get the XML comments working properly in the docs page, but am having trouble getting the to show. descriptions show just fine, but the remarks are missing completely.
My Swagger config includes c.IncludeXmlComments($#"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\XmlDocument.XML"); and I've confirmed the xml file contains the proper remarks.
All the properties are setup similar to this:
namespace My.Namespace
{
public class SomeRequestObject
{
/// <summary>
/// Some Property
/// </summary>
/// <remarks>
/// Details about this prop
/// More details about this prop
/// </remarks>
public string SomeProperty { get; set; }
}
}
I can see the remarks on the method calls themselves, but not on the object properties.
Any ideas on how to get the remarks to show in the UI?

Ok, so I couldn't find a built-in way to do this, but what I ended up doing was creating a custom schema filter and then "manually" added the remarks to the description.
SwaggerConfig.cs:
public class SwaggerConfig
{
public static void Register()
{
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
// ...Other config options
c.SchemaFilter<CustomSwaggerSchemaFilter>();
});
}
}
Then in the CustomSwaggerFilter.cs file I did:
public class CustomSwaggerSchemaFilter : ISchemaFilter
{
public void Apply(Schema outputSchema, SchemaRegistry schemaRegistry, Type inputType)
{
//Get properties and filter out dupes/empties
var props = inputType.GetProperties().ToList();
var baseProps = inputType.BaseType?.GetProperties().ToList();
if (baseProps != null && baseProps.Any(bp => props.Any(p => p.Name == bp.Name)))
{
baseProps.ForEach(bp =>
{
var indexToRemove = props.FindIndex(x => x.Name == bp.Name);
props.RemoveAt(indexToRemove);
props.Add(bp);
});
}
foreach (var prop in props)
{
//Get the remarks in the documentation
var propType = prop.ReflectedType;
var remarks = propType.GetDocumentationComment("remarks", 'P', prop.Name);
var outputProp = outputSchema.properties.FirstOrDefault(x => string.Equals(x.Key, prop.Name, StringComparison.OrdinalIgnoreCase));
if (outputProp.Value != null && !string.IsNullOrEmpty(remarks))
{
//Format remarks to display better
var formattedRemarks = string.Empty;
var remarkList = remarks.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var remark in remarkList)
{
formattedRemarks += $" \r\n_{remark.Trim()}_";
}
//Append to the description
outputProp.Value.description += formattedRemarks;
}
}
}
}
Which results in something like this:

Related

Storing secrets in ASP.NET Core for deploying at a shared hosting [duplicate]

With web.config going away, what is the preferred way to store sensitive info (passwords, tokens) in the configurations of a web app built using ASP.NET Core?
Is there a way to automatically get encrypted configuration sections in appsettings.json?
User secrets looks like a good solution for storing passwords, and, generally, application secrets, at least during development.
Check the official Microsoft documentation. You can also review this other SO question.
This is just a way to "hide" your secrets during development process and to avoid disclosing them into the source tree; the Secret Manager tool does not encrypt the stored secrets and should not be treated as a trusted store.
If you want to bring an encrypted appsettings.json to production, you can do so by building a custom configuration provider.
For example:
public class CustomConfigProvider : ConfigurationProvider, IConfigurationSource
{
public CustomConfigProvider()
{
}
public override void Load()
{
Data = UnencryptMyConfiguration();
}
private IDictionary<string, string> UnencryptMyConfiguration()
{
// do whatever you need to do here, for example load the file and unencrypt key by key
//Like:
var configValues = new Dictionary<string, string>
{
{"key1", "unencryptedValue1"},
{"key2", "unencryptedValue2"}
};
return configValues;
}
private IDictionary<string, string> CreateAndSaveDefaultValues(IDictionary<string, string> defaultDictionary)
{
var configValues = new Dictionary<string, string>
{
{"key1", "encryptedValue1"},
{"key2", "encryptedValue2"}
};
return configValues;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new CustomConfigProvider();
}
}
Define a static class for your extension method:
public static class CustomConfigProviderExtensions
{
public static IConfigurationBuilder AddEncryptedProvider(this IConfigurationBuilder builder)
{
return builder.Add(new CustomConfigProvider());
}
}
And then you can activate it:
// Set up configuration sources.
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEncryptedProvider()
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
I agree with #CoderSteve that writing a whole new provider is too much work. It also doesn't build on the existing standard JSON architecture. Here is a solution that I come up with the builds on top of the standard JSON architecture, uses the preferred .Net Core encryption libraries, and is very DI friendly.
public static class IServiceCollectionExtensions
{
public static IServiceCollection AddProtectedConfiguration(this IServiceCollection services)
{
services
.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(#"c:\keys"))
.ProtectKeysWithDpapi();
return services;
}
public static IServiceCollection ConfigureProtected<TOptions>(this IServiceCollection services, IConfigurationSection section) where TOptions: class, new()
{
return services.AddSingleton(provider =>
{
var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
section = new ProtectedConfigurationSection(dataProtectionProvider, section);
var options = section.Get<TOptions>();
return Options.Create(options);
});
}
private class ProtectedConfigurationSection : IConfigurationSection
{
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly IConfigurationSection _section;
private readonly Lazy<IDataProtector> _protector;
public ProtectedConfigurationSection(
IDataProtectionProvider dataProtectionProvider,
IConfigurationSection section)
{
_dataProtectionProvider = dataProtectionProvider;
_section = section;
_protector = new Lazy<IDataProtector>(() => dataProtectionProvider.CreateProtector(section.Path));
}
public IConfigurationSection GetSection(string key)
{
return new ProtectedConfigurationSection(_dataProtectionProvider, _section.GetSection(key));
}
public IEnumerable<IConfigurationSection> GetChildren()
{
return _section.GetChildren()
.Select(x => new ProtectedConfigurationSection(_dataProtectionProvider, x));
}
public IChangeToken GetReloadToken()
{
return _section.GetReloadToken();
}
public string this[string key]
{
get => GetProtectedValue(_section[key]);
set => _section[key] = _protector.Value.Protect(value);
}
public string Key => _section.Key;
public string Path => _section.Path;
public string Value
{
get => GetProtectedValue(_section.Value);
set => _section.Value = _protector.Value.Protect(value);
}
private string GetProtectedValue(string value)
{
if (value == null)
return null;
return _protector.Value.Unprotect(value);
}
}
}
Wire up your protected config sections like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Configure normal config settings
services.Configure<MySettings>(Configuration.GetSection("MySettings"));
// Configure protected config settings
services.AddProtectedConfiguration();
services.ConfigureProtected<MyProtectedSettings>(Configuration.GetSection("MyProtectedSettings"));
}
You can easily create encrypted values for your config files using a controller like this:
[Route("encrypt"), HttpGet, HttpPost]
public string Encrypt(string section, string value)
{
var protector = _dataProtectionProvider.CreateProtector(section);
return protector.Protect(value);
}
Usage:
http://localhost/cryptography/encrypt?section=SectionName:KeyName&value=PlainTextValue
I didn't want to write a custom provider – way too much work. I just wanted to tap into JsonConfigurationProvider, so I figured out a way that works for me, hope it helps someone.
public class JsonConfigurationProvider2 : JsonConfigurationProvider
{
public JsonConfigurationProvider2(JsonConfigurationSource2 source) : base(source)
{
}
public override void Load(Stream stream)
{
// Let the base class do the heavy lifting.
base.Load(stream);
// Do decryption here, you can tap into the Data property like so:
Data["abc:password"] = MyEncryptionLibrary.Decrypt(Data["abc:password"]);
// But you have to make your own MyEncryptionLibrary, not included here
}
}
public class JsonConfigurationSource2 : JsonConfigurationSource
{
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new JsonConfigurationProvider2(this);
}
}
public static class JsonConfigurationExtensions2
{
public static IConfigurationBuilder AddJsonFile2(this IConfigurationBuilder builder, string path, bool optional,
bool reloadOnChange)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("File path must be a non-empty string.");
}
var source = new JsonConfigurationSource2
{
FileProvider = null,
Path = path,
Optional = optional,
ReloadOnChange = reloadOnChange
};
source.ResolveFileProvider();
builder.Add(source);
return builder;
}
}
I managed to create a custom JSON configuration provider which uses DPAPI to encrypt and decrypt secrets. It basically uses simple regular expressions that you can define to specify what parts of the JSON needs to be encrypted.
The following steps are performed:
Json file is loaded
Determine whether the JSON parts that match the given regular expressions are already encrypted (or not). This is done by base-64 decoding of the JSON part and verify whether it starts with the expected prefix !ENC!)
If not encrypted, then encrypt the JSON part by first using DPAPI and secondly add the prefix !ENC! and encode to base-64
Overwrite the unencrypted JSON parts with the encrypted (base-64) values in the Json file
Note that the base-64 does not bring better security, but only hides the prefix !ENC! for cosmetic reasons. This is just a matter of taste of course ;)
This solution consists of the following classes:
ProtectedJsonConfigurationProvider class (= custom JsonConfigurationProvider)
ProtectedJsonConfigurationSource class (= custom JsonConfigurationSource)
AddProtectedJsonFile() extension method on the IConfigurationBuilder in order to simple add the protected configuration
Assuming the following initial authentication.json file:
{
"authentication": {
"credentials": [
{
user: "john",
password: "just a password"
},
{
user: "jane",
password: "just a password"
}
]
}
}
Which becomes (sort of) the following after loading
{
"authentication": {
"credentials": [
{
"user": "john",
"password": "IUVOQyEBAAAA0Iyd3wEV0R=="
},
{
"user": "jane",
"password": "IUVOQyEBAAAA0Iyd3wEV0R=="
}
]
}
}
And assuming the following configuration class based on the json format
public class AuthenticationConfiguration
{
[JsonProperty("credentials")]
public Collection<CredentialConfiguration> Credentials { get; set; }
}
public class CredentialConfiguration
{
[JsonProperty("user")]
public string User { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}
Below the sample code:
//Note that the regular expression will cause the authentication.credentials.password path to be encrypted.
//Also note that the byte[] contains the entropy to increase security
var configurationBuilder = new ConfigurationBuilder()
.AddProtectedJsonFile("authentication.json", true, new byte[] { 9, 4, 5, 6, 2, 8, 1 },
new Regex("authentication:credentials:[0-9]*:password"));
var configuration = configurationBuilder.Build();
var authenticationConfiguration = configuration.GetSection("authentication").Get<AuthenticationConfiguration>();
//Get the decrypted password from the encrypted JSON file.
//Note that the ProtectedJsonConfigurationProvider.TryGet() method is called (I didn't expect that :D!)
var password = authenticationConfiguration.Credentials.First().Password
Install the Microsoft.Extensions.Configuration.Binder package in order to get the configuration.GetSection("authentication").Get<T>() implementation
And finally the classes in which the magic happens :)
/// <summary>Represents a <see cref="ProtectedJsonConfigurationProvider"/> source</summary>
public class ProtectedJsonConfigurationSource : JsonConfigurationSource
{
/// <summary>Gets the byte array to increse protection</summary>
internal byte[] Entropy { get; private set; }
/// <summary>Represents a <see cref="ProtectedJsonConfigurationProvider"/> source</summary>
/// <param name="entropy">Byte array to increase protection</param>
/// <exception cref="ArgumentNullException"/>
public ProtectedJsonConfigurationSource(byte[] entropy)
{
this.Entropy = entropy ?? throw new ArgumentNullException(Localization.EntropyNotSpecifiedError);
}
/// <summary>Builds the configuration provider</summary>
/// <param name="builder">Builder to build in</param>
/// <returns>Returns the configuration provider</returns>
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new ProtectedJsonConfigurationProvider(this);
}
/// <summary>Gets or sets the protection scope of the configuration provider. Default value is <see cref="DataProtectionScope.CurrentUser"/></summary>
public DataProtectionScope Scope { get; set; }
/// <summary>Gets or sets the regular expressions that must match the keys to encrypt</summary>
public IEnumerable<Regex> EncryptedKeyExpressions { get; set; }
}
/// <summary>Represents a provider that protects a JSON configuration file</summary>
public partial class ProtectedJsonConfigurationProvider : JsonConfigurationProvider
{
private readonly ProtectedJsonConfigurationSource protectedSource;
private readonly HashSet<string> encryptedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private static readonly byte[] encryptedPrefixBytes = Encoding.UTF8.GetBytes("!ENC!");
/// <summary>Checks whether the given text is encrypted</summary>
/// <param name="text">Text to check</param>
/// <returns>Returns true in case the text is encrypted</returns>
private bool isEncrypted(string text)
{
if (text == null) { return false; }
//Decode the data in order to verify whether the decoded data starts with the expected prefix
byte[] decodedBytes;
try { decodedBytes = Convert.FromBase64String(text); }
catch (FormatException) { return false; }
return decodedBytes.Length >= encryptedPrefixBytes.Length
&& decodedBytes.AsSpan(0, encryptedPrefixBytes.Length).SequenceEqual(encryptedPrefixBytes);
}
/// <summary>Converts the given key to the JSON token path equivalent</summary>
/// <param name="key">Key to convert</param>
/// <returns>Returns the JSON token path equivalent</returns>
private string convertToTokenPath(string key)
{
var jsonStringBuilder = new StringBuilder();
//Split the key by ':'
var keyParts = key.Split(':');
for (var keyPartIndex = 0; keyPartIndex < keyParts.Length; keyPartIndex++)
{
var keyPart = keyParts[keyPartIndex];
if (keyPart.All(char.IsDigit)) { jsonStringBuilder.Append('[').Append(keyPart).Append(']'); }
else if (keyPartIndex > 0) { jsonStringBuilder.Append('.').Append(keyPart); }
else { jsonStringBuilder.Append(keyPart); }
}
return jsonStringBuilder.ToString();
}
/// <summary>Writes the given encrypted key/values to the JSON oconfiguration file</summary>
/// <param name="encryptedKeyValues">Encrypted key/values to write</param>
private void writeValues(IDictionary<string, string> encryptedKeyValues)
{
try
{
if (encryptedKeyValues == null || encryptedKeyValues.Count == 0) { return; }
using (var stream = new FileStream(this.protectedSource.Path, FileMode.Open, FileAccess.ReadWrite))
{
JObject json;
using (var streamReader = new StreamReader(stream, Encoding.UTF8, true, 4096, true))
{
using (var jsonTextReader = new JsonTextReader(streamReader))
{
json = JObject.Load(jsonTextReader);
foreach (var encryptedKeyValue in encryptedKeyValues)
{
var tokenPath = this.convertToTokenPath(encryptedKeyValue.Key);
var value = json.SelectToken(tokenPath) as JValue;
if (value.Value != null) { value.Value = encryptedKeyValue.Value; }
}
}
}
stream.Seek(0, SeekOrigin.Begin);
using (var streamWriter = new StreamWriter(stream))
{
using (var jsonTextWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented })
{
json.WriteTo(jsonTextWriter);
}
}
}
}
catch (Exception exception)
{
throw new Exception(string.Format(Localization.ProtectedJsonConfigurationWriteEncryptedValues, this.protectedSource.Path), exception);
}
}
/// <summary>Represents a provider that protects a JSON configuration file</summary>
/// <param name="source">Settings of the source</param>
/// <see cref="ArgumentNullException"/>
public ProtectedJsonConfigurationProvider(ProtectedJsonConfigurationSource source) : base(source)
{
this.protectedSource = source as ProtectedJsonConfigurationSource;
}
/// <summary>Loads the JSON data from the given <see cref="Stream"/></summary>
/// <param name="stream"><see cref="Stream"/> to load</param>
public override void Load(Stream stream)
{
//Call the base method first to ensure the data to be available
base.Load(stream);
var expressions = protectedSource.EncryptedKeyExpressions;
if (expressions != null)
{
//Dictionary that contains the keys (and their encrypted value) that must be written to the JSON file
var encryptedKeyValuesToWrite = new Dictionary<string, string>();
//Iterate through the data in order to verify whether the keys that require to be encrypted, as indeed encrypted.
//Copy the keys to a new string array in order to avoid a collection modified exception
var keys = new string[this.Data.Keys.Count];
this.Data.Keys.CopyTo(keys, 0);
foreach (var key in keys)
{
//Iterate through each expression in order to check whether the current key must be encrypted and is encrypted.
//If not then encrypt the value and overwrite the key
var value = this.Data[key];
if (!string.IsNullOrEmpty(value) && expressions.Any(e => e.IsMatch(key)))
{
this.encryptedKeys.Add(key);
//Verify whether the value is encrypted
if (!this.isEncrypted(value))
{
var protectedValue = ProtectedData.Protect(Encoding.UTF8.GetBytes(value), protectedSource.Entropy, protectedSource.Scope);
var protectedValueWithPrefix = new List<byte>(encryptedPrefixBytes);
protectedValueWithPrefix.AddRange(protectedValue);
//Convert the protected value to a base-64 string in order to mask the prefix (for cosmetic purposes)
//and overwrite the key with the encrypted value
var protectedBase64Value = Convert.ToBase64String(protectedValueWithPrefix.ToArray());
encryptedKeyValuesToWrite.Add(key, protectedBase64Value);
this.Data[key] = protectedBase64Value;
}
}
}
//Write the encrypted key/values to the JSON configuration file
this.writeValues(encryptedKeyValuesToWrite);
}
}
/// <summary>Attempts to get the value of the given key</summary>
/// <param name="key">Key to get</param>
/// <param name="value">Value of the key</param>
/// <returns>Returns true in case the key has been found</returns>
public override bool TryGet(string key, out string value)
{
if (!base.TryGet(key, out value)) { return false; }
else if (!this.encryptedKeys.Contains(key)) { return true; }
//Key is encrypted and must therefore be decrypted in order to return.
//Note that the decoded base-64 bytes contains the encrypted prefix which must be excluded when unprotection
var protectedValueWithPrefix = Convert.FromBase64String(value);
var protectedValue = new byte[protectedValueWithPrefix.Length - encryptedPrefixBytes.Length];
Buffer.BlockCopy(protectedValueWithPrefix, encryptedPrefixBytes.Length, protectedValue, 0, protectedValue.Length);
var unprotectedValue = ProtectedData.Unprotect(protectedValue, this.protectedSource.Entropy, this.protectedSource.Scope);
value = Encoding.UTF8.GetString(unprotectedValue);
return true;
}
/// <summary>Provides extensions concerning <see cref="ProtectedJsonConfigurationProvider"/></summary>
public static class ProtectedJsonConfigurationProviderExtensions
{
/// <summary>Adds a protected JSON file</summary>
/// <param name="configurationBuilder"><see cref="IConfigurationBuilder"/> in which to apply the JSON file</param>
/// <param name="path">Path to the JSON file</param>
/// <param name="optional">Specifies whether the JSON file is optional</param>
/// <param name="entropy">Byte array to increase protection</param>
/// <returns>Returns the <see cref="IConfigurationBuilder"/></returns>
/// <exception cref="ArgumentNullException"/>
public static IConfigurationBuilder AddProtectedJsonFile(this IConfigurationBuilder configurationBuilder, string path, bool optional, byte[] entropy, params Regex[] encryptedKeyExpressions)
{
var source = new ProtectedJsonConfigurationSource(entropy)
{
Path = path,
Optional = optional,
EncryptedKeyExpressions = encryptedKeyExpressions
};
return configurationBuilder.Add(source);
}
}
public static IServiceCollection ConfigureProtected<TOptions>(this IServiceCollection services, IConfigurationSection section) where TOptions: class, new()
{
return services.AddSingleton(provider =>
{
var dataProtectionProvider = provider.GetRequiredService<IDataProtectionProvider>();
var protectedSection = new ProtectedConfigurationSection(dataProtectionProvider, section);
var options = protectedSection.Get<TOptions>();
return Options.Create(options);
});
}
This method is correct
Just a few clarifications to help avoid problems. When you encrypt a value, it's using the section as 'Purpose' (https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/purpose-strings?view=aspnetcore-2.2) When you get a 'Payload not valid' or something similar, it's likely that the purpose you used to encrypt it, differs from the purpose use to decrypt it. So, let's say I have a first level section in my appsettings.json named 'SecureSettings' and within it a connection string:
{
"SecureSettings":
{
"ConnectionString":"MyClearTextConnectionString"
}
}
To encrypt the value, I'd call: http://localhost/cryptography/encrypt?section=SecureSettings:ConnectionString&value=MyClearTextConnectionString
You may not want to keep an Encrypt controller in the app itself btw.

I get ajax Requested page not found error what is wrong with this [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed 3 years ago.
Improve this question
When I do a POST I always get the 'Requested page not found. [404]', I cant figure out whats´wrong please advice
I have this web API with ASP.NET Core MVC project that works great and now I modify it, remove database and add custom ControllerBase instead.
But I get the [404] error somethings missing ..
here is my code
JavaScript
const uri = "api/book";
let todos = null;
function getCount(data) {
const el = $("#counter");
let name = "to-do";
if (data) {
if (data > 1) {
name = "to-dos";
}
el.text(data + " " + name);
} else {
el.text("No " + name);
}
}
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: "GET",
url: uri,
cache: false,
success: function (data) {
const tBody = $("#todos");
$(tBody).empty();
getCount(data.length);
$.each(data, function (key, item) {
const tr = $("<tr></tr>")
.append(
$("<td></td>").append(
$("<input/>", {
type: "checkbox",
disabled: true,
checked: item.isComplete
})
)
)
.append($("<td></td>").text(item.name))
.append(
$("<td></td>").append(
$("<button>Edit</button>").on("click", function () {
editItem(item.id);
})
)
)
.append(
$("<td></td>").append(
$("<button>Delete</button>").on("click", function () {
deleteItem(item.id);
})
)
);
tr.appendTo(tBody);
});
todos = data;
}
});
}
function addItem() {
const item = {
name: $("#add-name").val(),
isComplete: false
};
$.ajax({
type: "POST",
accepts: "application/json",
url: uri,
contentType: "application/json",
data: JSON.stringify(item),
error: function (jqXHR, textStatus, errorThrown) {
///alert("Something went wrong!");
var msg = '';
if (jqXHR.status === 0) {
msg = 'Not connect.\n Verify Network.';
} else if (jqXHR.status == 404) {
msg = 'Requested page not found. [404]';
} else if (jqXHR.status == 500) {
msg = 'Internal Server Error [500].';
} else if (exception === 'parsererror') {
msg = 'Requested JSON parse failed.';
} else if (exception === 'timeout') {
msg = 'Time out error.';
} else if (exception === 'abort') {
msg = 'Ajax request aborted.';
} else {
msg = 'Uncaught Error.\n' + jqXHR.responseText;
}
alert(msg);
///$('#post').html(msg);
},
success: function (result) {
getData();
$("#add-name").val("");
}
});
}
function deleteItem(id) {
$.ajax({
url: uri + "/" + id,
type: "DELETE",
success: function (result) {
getData();
}
});
}
function editItem(id) {
$.each(todos, function (key, item) {
if (item.id === id) {
$("#edit-name").val(item.name);
$("#edit-id").val(item.id);
$("#edit-isComplete")[0].checked = item.isComplete;
}
});
$("#spoiler").css({ display: "block" });
}
$(".my-form").on("submit", function () {
const item = {
name: $("#edit-name").val(),
isComplete: $("#edit-isComplete").is(":checked"),
id: $("#edit-id").val()
};
$.ajax({
url: uri + "/" + $("#edit-id").val(),
type: "PUT",
accepts: "application/json",
contentType: "application/json",
data: JSON.stringify(item),
success: function (result) {
getData();
}
});
closeInput();
return false;
});
function closeInput() {
$("#spoiler").css({ display: "none" });
}
My Controller
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Xml.Linq;
using WorkSampleBookSearch.Model;
namespace WorkSampleBookSearch
{
[Route("api/[controller]")]
[ApiController]
public class BooksXmlController : ControllerBase
{
public BooksXmlController()
{
}
/// <summary>
/// Retrieve all items from PhoneBook.
/// </summary>
/// <returns>PhoneBook items List</returns>
// GET: api/Book
[HttpGet]
public IActionResult GetPhoneBookItems()
{
List<PhoneBookItem> PhoneBookItems = new List<PhoneBookItem>();
XDocument doc = XDocument.Load("books.xml");
foreach (XElement element in doc.Descendants("phonebookitems")
.Descendants("phonebookitem"))
{
PhoneBookItem phonebookitem = new PhoneBookItem
{
/// Id
/// Author
/// Title
/// Genre
/// Price
/// Publish_date
/// Description
Id = element.Element("id").Value,
Author = element.Element("author").Value,
Title = element.Element("title").Value,
Genre = element.Element("genre").Value,
Price = element.Element("price").Value,
Publish_date = element.Element("publish_date").Value,
Description = element.Element("description").Value
};
PhoneBookItems.Add(phonebookitem);
PhoneBookItems = PhoneBookItems.OrderBy(p => p.Title).ToList();
}
return Ok(PhoneBookItems);
}
/// <summary>
/// Returns a PhoneBook item matching the given id.
/// </summary>
/// <param name="id">Id of item to be retrieved</param>
/// <returns>PhoneBook item</returns>
// GET: api/Book/5
[HttpGet("{id}")]
public IActionResult GetPhoneBookItem(long id)
{
XDocument doc = XDocument.Load("books.xml");
XElement element = doc.Element("phonebookitems").Elements("phonebookitem").
Elements("id").SingleOrDefault(x => x.Value == id.ToString());
XElement parent = element.Parent;
PhoneBookItem phonebookitem = new PhoneBookItem
{
Id = element.Element("id").Value,
Author = element.Element("author").Value,
Title = element.Element("title").Value,
Genre = element.Element("genre").Value,
Price = element.Element("price").Value,
Publish_date = element.Element("publish_date").Value,
Description = element.Element("description").Value
};
return Ok(phonebookitem);
}
/// <summary>
/// Insert a PhoneBook item.
/// </summary>
/// <returns>Inserts new PhoneBook item in books.xml and saves the file</returns>
//POST: api/Book
[HttpPost]
public void PostPhoneBookItem(PhoneBookItem PhoneBookItem)
{
int maxId;
XDocument doc = XDocument.Load("books.xml");
bool elementExist = doc.Descendants("phonebookitem").Any();
if (elementExist)
{
maxId = doc.Descendants("phonebookitem").Max(x => (int)x.Element("id"));
}
else
{
maxId = 0;
}
/// Id
/// Author
/// Title
/// Genre
/// Price
/// Price
/// Publish_date
/// Description
XElement root = new XElement("phonebookitem");
root.Add(new XElement("id", maxId + 1));
root.Add(new XElement("author", PhoneBookItem.Author));
root.Add(new XElement("title", PhoneBookItem.Title));
root.Add(new XElement("genre", PhoneBookItem.Genre));
root.Add(new XElement("price", PhoneBookItem.Price));
root.Add(new XElement("publish_date", PhoneBookItem.Publish_date));
root.Add(new XElement("description", PhoneBookItem.Description));
doc.Element("phonebookitems").Add(root);
doc.Save("books.xml");
}
/// <summary>
/// Updates a PhoneBook item matching the given id.
/// </summary>
/// <param name="id">Id of item to be retrieved</param>
/// <param name="PhoneBookItem">Retrieved PhoneBook item</param>
/// <returns>Updates PhoneBook item in books.xml and saves the file</returns>
//PUT: api/Book/5
[HttpPut("{id}")]
public void PostPhoneBookItem(long id, PhoneBookItem PhoneBookItem)
{
XDocument doc = XDocument.Load("books.xml");
var items = from item in doc.Descendants("phonebookitem")
where item.Element("id").Value == id.ToString()
select item;
foreach (XElement itemElement in items)
{
/// Id
/// Author
/// Title
/// Genre
/// Price
/// Publish_date
/// Description
itemElement.SetElementValue("author", PhoneBookItem.Author);
itemElement.SetElementValue("title", PhoneBookItem.Title);
itemElement.SetElementValue("genre", PhoneBookItem.Genre);
itemElement.SetElementValue("price", PhoneBookItem.Price);
itemElement.SetElementValue("publish_date", PhoneBookItem.Publish_date);
itemElement.SetElementValue("description", PhoneBookItem.Description);
}
doc.Save("books.xml");
}
/// <summary>
/// Delete a PhoneBook item matching the given id.
/// </summary>
/// <param name="id">Id of item to be retrieved</param>
/// <returns>Deletes item from books.xml and saves the file</returns>
// DELETE: api/Book/5
[HttpDelete("{id}")]
public void DeletePhoneBookItem(long id)
{
XDocument doc = XDocument.Load("books.xml");
var elementToDelete = from item in doc.Descendants("phonebookitem")
where item.Element("id").Value == id.ToString()
select item;
elementToDelete.Remove();
doc.Save("books.xml");
}
}
}
Here is my startup
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace WorkSampleBookSearch
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
Change the URI from
const uri = "api/book";
to
const uri = "api/booksxml";
Explanation:
[Route("api/[controller]")] means that the route will be be api/controllerName and in your case the name of the controller is BooksXml (the Controller suffix is not taken into consideration).

Configure mapFrom expression just once in Automapper for JObject for a common access pattern

I am mapping from a JObject to a custom class and it works fine, but i'd rather tell it to do this default type of mapping once instead of having to do a .MapFrom for every property. Most of the source "properties" are just the lowercase underscored version of the Pascal case property names on the destination.
Since JObject doesnt have the values I want as properties, i have to index into them via the MapFrom (I can't use SourceMemberNamingConvention/DestinationMemberNamingConvention). So I want an expression that i can use via something like ForAllOtherMembers to apply this default value retrieval from the JObject but I can't it to work...what options do i have?
I have tried using ResolveUsing method that I would have to also use ConstructedBy method but ResolveUsing returns a void (now? in version 5) so I couldn't do a .ConstructedBy() on it.
Below I am having to do each MapFrom even though they follow the same access pattern:
MapperConfiguration MapperConfig = new MapperConfiguration(cfg => {
cfg.CreateMap<string, bool>().ConvertUsing<BooleanTypeConverter>();
cfg.CreateMap<JObject, FiscalYear>()
.ForMember("EmployeeName",
options => options.MapFrom(jo => jo["employee_name"]))
.ForMember("YearName",
options => options.MapFrom(jo => jo["year_name"]));
UPDATE:
I went ahead and just did it more the manual way and replaced strings with a delegate
public static class AutoMapperConfig
{
public static MapperConfiguration MapperConfig = new MapperConfiguration(cfg => {
cfg.CreateMap<string, bool>().ConvertUsing<BooleanTypeConverter>();
cfg.CreateMap<JToken, FiscalYear>()
.Map(d => d.EmployeeName)
.Map(d => d.Year, "int_year")
.Map(d => d.Name, "year");
});
}
public static class MappingExpressionExtensions
{
private static readonly MatchEvaluator ToSnakeCaseEvaluator = m =>
{
string match = m.ToString();
return "_" + char.ToLower(match[0]) + match.Substring(1);
};
public static IMappingExpression<JToken, TDest> Map<TDest, TDestProp>(this IMappingExpression<JToken, TDest> map,
Expression<Func<TDest, TDestProp>> propertyExpression, string sourceName = null)
{
var propertyName = propertyExpression.PropertyName();
sourceName = string.IsNullOrWhiteSpace(sourceName)
? new Regex("([A-Z])").Replace(propertyName, ToSnakeCaseEvaluator).TrimStart('_')
: sourceName;
var propType = typeof(TDestProp);
if (propType == typeof(bool))
{
// in order for BooleanTypeConverter to work, convert to string and run as a normal mapping
map.ForMember(propertyExpression.PropertyName(), o => o.MapFrom(jo => jo[sourceName].ToString()));
return map;
}
var isNullableGenericType = propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(Nullable<>);
var underlyingType = isNullableGenericType ? Nullable.GetUnderlyingType(propType) : propType;
TypeConverter converter = TypeDescriptor.GetConverter(underlyingType);
map.ForMember(propertyName,
o => o.MapFrom(jo => (isNullableGenericType && string.IsNullOrWhiteSpace(jo[sourceName].ToString()))
|| (IsNumeric(jo[sourceName]) && string.IsNullOrWhiteSpace(jo[sourceName].ToString()))
? 0 : converter.ConvertFrom(jo[sourceName].ToString())));
return map;
}
public static bool IsNumeric(object expression)
{
if (expression == null)
return false;
double number;
return Double.TryParse(Convert.ToString(expression, CultureInfo.InvariantCulture),
NumberStyles.Any, NumberFormatInfo.InvariantInfo, out number);
}
}
public class BooleanTypeConverter : ITypeConverter<string, bool>
{
/// <summary>
/// Automapper version compatible with version 4.x
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public bool Convert(ResolutionContext context)
{
switch (context.SourceValue.ToString().ToLower().Trim())
{
case "true":
case "yes":
case "y":
case "1":
return true;
}
return false;
}
//// Automapper version compatible with version 5.0+
//public bool Convert(string source, bool destination, ResolutionContext context)
//{
// switch (source.ToLower().Trim())
// {
// case "true":
// case "yes":
// case "y":
// case "1":
// return true;
// }
// return false;
//}
// put this in static class
public static string PropertyName<T, TProperty>(this Expression<Func<T, TProperty>> property)
{
if (property.Body is MemberExpression)
return ((MemberExpression) property.Body).Member.Name;
return ((MemberExpression) ((UnaryExpression) property.Body).Operand).Member.Name;
}
}

Validate Modified Model Using Annotations in EntityFramwork and ASPNET

I have this class as a part of EF Model:
class Person {
public int Id { get; set; }
[MaxLength(100, ErrorMessage="Name cannot be more than 100 characters")]
public string Name { get; set; }
}
And I have this method in my controller:
public IActionResult ChangeName(int id, string name) {
var person = db.Persons.Find(id);
if(person == null) return NotFound();
person.Name = name;
db.SaveChanges();
return Json(new {result = "Saved Successfully"});
}
Is there any way to validate person after changing the Name property using the annotation MaxLength rather than manually check for it. Becuase sometimes I might have more than one validation and I don't want to examine each one of them. Also, I might change these parameters in the future (e.g. make the max length 200), and that means I have to change it everywhere else.
So is it possible?
Your method works as long as there is one validation error per property. Also, it's quite elaborate. You can use db.GetValidationErrors() to get the same result. One difference is that errors are collected in a collection per property name:
var errors = db.GetValidationErrors()
.SelectMany(devr => devr.ValidationErrors)
.GroupBy(ve => ve.PropertyName)
.ToDictionary(ve => ve.Key, ve => ve.Select(v => v.ErrorMessage));
Okay, I found a solution to my problem, I created a method that takes the model and checks for errors:
private IDictionary<string, string> ValidateModel(Person model)
{
var errors = new Dictionary<string, string>();
foreach (var property in model.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes())
{
var validationAttribute = attribute as ValidationAttribute;
if(validationAttribute == null) continue;
var value = property.GetValue(model);
if (!validationAttribute.IsValid(value))
{
errors.Add(property.Name, validationAttribute.ErrorMessage);
}
}
}
return errors;
}
UPDATE:
As stated by #Gert Arnold, the method above returns only one validation per property. Below is the fixed version which returns a list of errors for each property
public static IDictionary<string, IList<string>> ValidateModel(Person model)
{
var errors = new Dictionary<string, IList<string>>();
foreach (var property in model.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes())
{
var validationAttribute = attribute as ValidationAttribute;
if (validationAttribute == null) continue;
var value = property.GetValue(model);
if (validationAttribute.IsValid(value)) continue;
if (!errors.ContainsKey(property.Name))
errors[property.Name] = new List<string>();
errors[property.Name].Add(validationAttribute.ErrorMessage);
}
}
return errors;
}

Converting List in Comma Separated List with linq?

I am having a info class with following field like id,name,address and i have created list of that info like List.
I want to have all the values of list into comma seperated strings using linq. Is there any way for this.
simple way... may not be the fastest so it depends on how many records you are processing
var myObjects = new[] {
new {
id=1,
name="Matt",
address="1234 no chance ln\r\nnowhere, OH 12345"
},
new {
id=1,
name="Jim",
address="4321 no chance ln\r\nnowhere, OH 12345"
}
};
var myList = (from o in myObjects
select string.Format("{0},\"{1}\",\"{2}\"",
o.id,
o.name,
(o.address ?? string.Empty).Replace("\r\n", ";")
)).ToList();
Have a look at this example by Mike Hadlow. It needs some improvement (escaping commas, new line support etc) but gives you the basic idea.
Taken from the LinQExtensions.cs found at Batch Updates and Deletes with LINQ to SQL
/// <summary>
/// Creates a *.csv file from an IQueryable query, dumping out the 'simple' properties/fields.
/// </summary>
/// <param name="query">Represents a SELECT query to execute.</param>
/// <param name="fileName">The name of the file to create.</param>
/// <remarks>
/// <para>If the file specified by <paramref name="fileName"/> exists, it will be deleted.</para>
/// <para>If the <paramref name="query"/> contains any properties that are entity sets (i.e. rows from a FK relationship) the values will not be dumped to the file.</para>
/// <para>This method is useful for debugging purposes or when used in other utilities such as LINQPad.</para>
/// </remarks>
public static void DumpCSV(this IQueryable query, string fileName)
{
query.DumpCSV(fileName, true);
}
/// <summary>
/// Creates a *.csv file from an IQueryable query, dumping out the 'simple' properties/fields.
/// </summary>
/// <param name="query">Represents a SELECT query to execute.</param>
/// <param name="fileName">The name of the file to create.</param>
/// <param name="deleteFile">Whether or not to delete the file specified by <paramref name="fileName"/> if it exists.</param>
/// <remarks>
/// <para>If the <paramref name="query"/> contains any properties that are entity sets (i.e. rows from a FK relationship) the values will not be dumped to the file.</para>
/// <para>This method is useful for debugging purposes or when used in other utilities such as LINQPad.</para>
/// </remarks>
public static void DumpCSV(this IQueryable query, string fileName, bool deleteFile)
{
if (File.Exists(fileName) && deleteFile)
{
File.Delete(fileName);
}
using (var output = new FileStream(fileName, FileMode.CreateNew))
{
using (var writer = new StreamWriter(output))
{
var firstRow = true;
PropertyInfo[] properties = null;
FieldInfo[] fields = null;
Type type = null;
bool typeIsAnonymous = false;
foreach (var r in query)
{
if (type == null)
{
type = r.GetType();
typeIsAnonymous = type.IsAnonymous();
properties = type.GetProperties();
fields = type.GetFields();
}
var firstCol = true;
if (typeIsAnonymous)
{
if (firstRow)
{
foreach (var p in properties)
{
if (!firstCol) writer.Write(",");
else { firstCol = false; }
writer.Write(p.Name);
}
writer.WriteLine();
}
firstRow = false;
firstCol = true;
foreach (var p in properties)
{
if (!firstCol) writer.Write(",");
else { firstCol = false; }
DumpValue(p.GetValue(r, null), writer);
}
}
else
{
if (firstRow)
{
foreach (var p in fields)
{
if (!firstCol) writer.Write(",");
else { firstCol = false; }
writer.Write(p.Name);
}
writer.WriteLine();
}
firstRow = false;
firstCol = true;
foreach (var p in fields)
{
if (!firstCol) writer.Write(",");
else { firstCol = false; }
DumpValue(p.GetValue(r), writer);
}
}
writer.WriteLine();
}
}
}
}
private static void DumpValue(object v, StreamWriter writer)
{
if (v != null)
{
switch (Type.GetTypeCode(v.GetType()))
{
// csv encode the value
case TypeCode.String:
string value = (string)v;
if (value.Contains(",") || value.Contains('"') || value.Contains("\n"))
{
value = value.Replace("\"", "\"\"");
if (value.Length > 31735)
{
value = value.Substring(0, 31732) + "...";
}
writer.Write("\"" + value + "\"");
}
else
{
writer.Write(value);
}
break;
default: writer.Write(v); break;
}
}
}
private static bool IsAnonymous(this Type type)
{
if (type == null)
throw new ArgumentNullException("type");
// HACK: The only way to detect anonymous types right now.
return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
&& type.IsGenericType && type.Name.Contains("AnonymousType")
&& (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
&& (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic;
}

Resources