How do you model bind an array from the URI with GET in ASP.NET Core 1 Web API (implicitly or explicitly)?
In ASP.NET Web API pre Core 1, this worked:
[HttpGet]
public void Method([FromUri] IEnumerable<int> ints) { ... }
How do you do this in ASP.NET Web API Core 1 (aka ASP.NET 5 aka ASP.NET vNext)? The docs have nothing.
The FromUriAttribute class combines the FromRouteAttribute and FromQueryAttribute classes. Depending the configuration of your routes / the request being sent, you should be able to replace your attribute with one of those.
However, there is a shim available which will give you the FromUriAttribute class. Install the "Microsoft.AspNet.Mvc.WebApiCompatShim" NuGet package through the package explorer, or add it directly to your project.json file:
"dependencies": {
"Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
}
While it is a little old, I've found that this article does a pretty good job of explaining some of the changes.
Binding
If you're looking to bind comma separated values for the array ("/api/values?ints=1,2,3"), you will need a custom binder just as before. This is an adapted version of Mrchief's solution for use in ASP.NET Core.
public class CommaDelimitedArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.IsEnumerableType)
{
var key = bindingContext.ModelName;
var value = bindingContext.ValueProvider.GetValue(key).ToString();
if (!string.IsNullOrWhiteSpace(value))
{
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
var converter = TypeDescriptor.GetConverter(elementType);
var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(x.Trim()))
.ToArray();
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
// change this line to null if you prefer nulls to empty arrays
bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(bindingContext.ModelType.GetElementType(), 0));
}
return TaskCache.CompletedTask;
}
return TaskCache.CompletedTask;
}
}
You can either specify the model binder to be used for all collections in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc().AddMvcOptions(opts =>
{
opts.ModelBinders.Insert(0, new CommaDelimitedArrayModelBinder());
});
}
Or specify it once in your API call:
[HttpGet]
public void Method([ModelBinder(BinderType = typeof(CommaDelimitedArrayModelBinder))] IEnumerable<int> ints)
ASP.NET Core 1.1 Answer
#WillRay's answer is a little outdated. I have written an 'IModelBinder' and 'IModelBinderProvider'. The first can be used with the [ModelBinder(BinderType = typeof(DelimitedArrayModelBinder))] attribute, while the second can be used to apply the model binder globally as I've show below.
.AddMvc(options =>
{
// Add to global model binders so you don't need to use the [ModelBinder] attribute.
var arrayModelBinderProvider = options.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
options.ModelBinderProviders.Insert(
options.ModelBinderProviders.IndexOf(arrayModelBinderProvider),
new DelimitedArrayModelBinderProvider());
})
public class DelimitedArrayModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsEnumerableType && !context.Metadata.ElementMetadata.IsComplexType)
{
return new DelimitedArrayModelBinder();
}
return null;
}
}
public class DelimitedArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
var values = valueProviderResult
.ToString()
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
if (values.Length == 0)
{
bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(elementType, 0));
}
else
{
var converter = TypeDescriptor.GetConverter(elementType);
var typedArray = Array.CreateInstance(elementType, values.Length);
try
{
for (int i = 0; i < values.Length; ++i)
{
var value = values[i];
var convertedValue = converter.ConvertFromString(value);
typedArray.SetValue(convertedValue, i);
}
}
catch (Exception exception)
{
bindingContext.ModelState.TryAddModelError(
modelName,
exception,
bindingContext.ModelMetadata);
}
bindingContext.Result = ModelBindingResult.Success(typedArray);
}
return Task.CompletedTask;
}
}
There are some changes in the .NET Core 3.
Microsoft has split out the functionality from the AddMvc method (source).
As AddMvc also includes support for View Controllers, Razor Views and etc. If you don't need to use them in your project (like in an API), you might consider using services.AddControllers() which is for Web API controllers.
So, updated code will look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddMvcOptions(opt =>
{
var mbp = opt.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
opt.ModelBinderProviders.Insert(opt.ModelBinderProviders.IndexOf(mbp), new DelimitedArrayModelBinderProvider());
});
}
Related
I structured my project into multiple mobile services, grouped by the application type eg:
my-core.azure-mobile.net (user, device)
my-app-A.azure-mobile.net (sales, order, invoice)
my-app-B.azure-mobile.net (inventory & parts)
I'm using custom authentication for all my services, and I implemented my own SSO by setting the same master key to all 3 services.
Things went well when I tested using REST client, eg. user who "logged in" via custom api at my-core.azure-mobile.net is able to use the returned JWT token to access restricted API of the other mobile services.
However, in my xamarin project, only the first (note, in sequence of creation) MobileServiceClient object is working properly (eg. returning results from given table). The client object are created using their own url and key respectively, and stored in a dictionary.
If i created client object for app-A then only create for app-B, I will be able to perform CRUD+Sync on sales/order/invoice entity, while CRUD+Sync operation on inventory/part entity will just hang there. The situation is inverse if I swap the client object creation order.
I wonder if there is any internal static variables used within the MobileServiceClient which caused such behavior, or it is a valid bug ?
=== code snippet ===
public class AzureService
{
IDictionary<String, MobileServiceClient> services = new Dictionary<String, MobileServiceClient>();
public MobileServiceClient Init (String key, String applicationURL, String applicationKey)
{
return services[key] = new MobileServiceClient (applicationURL, applicationKey);
}
public MobileServiceClient Get(String key)
{
return services [key];
}
public void InitSyncContext(MobileServiceSQLiteStore offlineStore)
{
// Uses the default conflict handler, which fails on conflict
// To use a different conflict handler, pass a parameter to InitializeAsync.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=521416
var syncHandler = new MobileServiceSyncHandler ();
foreach(var client in services) {
client.Value.SyncContext.InitializeAsync (offlineStore, syncHandler);
}
}
public void SetAuthenticationToken(String uid, String token)
{
var user = new MobileServiceUser(uid);
foreach(var client in services) {
client.Value.CurrentUser = user;
client.Value.CurrentUser.MobileServiceAuthenticationToken = token;
}
}
public void ClearAuthenticationToken()
{
foreach(var client in services) {
client.Value.CurrentUser = null;
}
}
}
=== more code ===
public class DatabaseService
{
public static MobileServiceSQLiteStore LocalStore = null;
public static string Path { get; set; }
public static ISet<IEntityMappingProvider> Providers = new HashSet<IEntityMappingProvider> ();
public static void Init (String dbPath)
{
LocalStore = new MobileServiceSQLiteStore(dbPath);
foreach(var provider in Providers) {
var types = provider.GetSupportedTypes ();
foreach(var t in types) {
JObject item = null;
// omitted detail to create JObject using reflection on given type
LocalStore.DefineTable(tableName, item);
}
}
}
}
=== still code ===
public class AzureDataSyncService<T> : IAzureDataSyncService<T>
{
public MobileServiceClient ServiceClient { get; set; }
public virtual Task<List<T>> GetAll()
{
try
{
var theTable = ServiceClient.GetSyncTable<T>();
return theTable.ToListAsync();
}
catch (MobileServiceInvalidOperationException msioe)
{
Debug.WriteLine("GetAll<{0}> EXCEPTION TYPE: {1}, EXCEPTION:{2}", typeof(T).ToString(), msioe.GetType().ToString(), msioe.ToString());
}
catch (Exception e)
{
Debug.WriteLine("GetAll<{0}> EXCEPTION TYPE: {1}, EXCEPTION:{2}", typeof(T).ToString(), e.GetType().ToString(), e.ToString());
}
List<T> theCollection = Enumerable.Empty<T>().ToList();
return Task.FromResult(theCollection);
}
}
=== code ===
public class UserService : AzureDataSyncService<User>
{
}
public class PartService : AzureDataSyncService<Part>
{
}
const string coreApiURL = #"https://my-core.azure-mobile.net/";
const string coreApiKey = #"XXXXX";
const string invApiURL = #"https://my-inventory.azure-mobile.net/";
const string invApiKey = #"YYYYY";
public async void Foo ()
{
DatabaseService.Providers.Add (new CoreDataMapper());
DatabaseService.Providers.Add (new InvDataMapper ());
DatabaseService.Init (DatabaseService.Path);
var coreSvc = AzureService.Instance.Init ("Core", coreApiURL, coreApiKey);
var invSvc = AzureService.Instance.Init ("Inv", invApiURL, invApiKey);
AzureService.Instance.InitSyncContext (DatabaseService.LocalStore);
AzureService.Instance.SetAuthenticationToken("AAA", "BBB");
UserService.Instance.ServiceClient = coreSvc;
PartService.Instance.ServiceClient = invSvc;
var x = await UserService.GetAll(); // this will work
var y = await PartService.GetAll(); // but not this
}
It's ok to use multiple MobileServiceClient objects, but not with the same local database. The offline sync feature uses a particular system tables to keep track of table operations and errors, and it is not supported to use the same local store across multiple sync contexts.
I'm not totally sure why it is hanging in your test, but it's possible that there is a lock on the local database file and the other sync context is waiting to get access.
You should instead use different local database files for each service and doing push and pull on each sync context. With your particular example, you just need to move LocalStore out of DatabaseService and into a dictionary in AzureService.
In general, it seems like an unusual design to use multiple services from the same client app. Is there a particular reason that the services need to be separated from each other?
I am attempting to write a Validation attribute in MVC4.
The purpose is to check for the existence of an application reference (just a string that represents a key I wish to prevent a duplicate for).
My data is accessed via WebAPI and because I am using 4.5 I wish to make this asynchronous if possible.
I am perhaps not making the best or appropriate usage of async and await but I would like to know how to call my async method from the overridden IsValid method of the inherited Validation class.
public class UniqueApplicationReferenceAttribute : ValidationAttribute
{
public UniqueApplicationReferenceAttribute() : base(() => "The {0} already exists") { }
public int? ApplicationCount { get; set; }
public override bool IsValid(object value)
{
var myTask = GetApplicationRefCountAsync();
myTask.Wait();
this.ApplicationCount = this.ApplicationCount ?? 0;
if (ApplicationCount > 0)
{
return true;
}
else
{
return false;
}
}
public async Task GetApplicationRefCountAsync()
{
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("http://localhost:11111/");
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
var apps = client.GetStringAsync("api/dataapplications");
await Task.WhenAll(apps);
var appList = apps.Result;
this.ApplicationCount = appList.Count();// apps.Count();
}
}
Many thanks,
Dan.
I recommend that you call your WebAPI methods synchronously. ValidationAttribute does not support asynchronous implementations natively, so any synchronous-over-asynchronous code you'll write is just going to be a hack and not actually provide any benefit as compared to the synchronous version.
I'm not able to test this in full, but you should be able to do something like this:
public bool IsValid(object value)
{
var appCount = GetApplicationRefCountAsync().Result;
return appCount > 0;
}
public async Task<int> GetApplicationRefCountAsync()
{
var client = new HttpClient();
client.BaseAddress = new Uri("http://localhost:11111/");
client.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
return await client.GetStringAsync("api/dataapplications")
.ContinueWith(r => Convert.ToInt32(r))
.ConfigureAwait(false);
}
Be careful about using async/await methods in an ASP.NET thread. It's easy to create deadlocks.
I'm creating my own IntelliSense Presenter, since Visual Studio2012 support change theme, so I want my background color of the presenter can be auto-changed when the theme been changed. Is there a way to track the theme changes event, or get the current color theme of the Visual Studio?
Yes, this is possible. I had to solve a similiar issue with one of my extensions...
The current theme is stored in the Windows Registry; so I implemented the following utility class.
public enum VsTheme
{
Unknown = 0,
Light,
Dark,
Blue
}
public class ThemeUtil
{
private static readonly IDictionary<string, VsTheme> Themes = new Dictionary<string, VsTheme>()
{
{ "de3dbbcd-f642-433c-8353-8f1df4370aba", VsTheme.Light },
{ "1ded0138-47ce-435e-84ef-9ec1f439b749", VsTheme.Dark },
{ "a4d6a176-b948-4b29-8c66-53c97a1ed7d0", VsTheme.Blue }
};
public static VsTheme GetCurrentTheme()
{
string themeId = GetThemeId();
if (string.IsNullOrWhiteSpace(themeId) == false)
{
VsTheme theme;
if (Themes.TryGetValue(themeId, out theme))
{
return theme;
}
}
return VsTheme.Unknown;
}
public static string GetThemeId()
{
const string CategoryName = "General";
const string ThemePropertyName = "CurrentTheme";
string keyName = string.Format(#"Software\Microsoft\VisualStudio\11.0\{0}", CategoryName);
using (RegistryKey key = Registry.CurrentUser.OpenSubKey(keyName))
{
if (key != null)
{
return (string)key.GetValue(ThemePropertyName, string.Empty);
}
}
return null;
}
}
Okay; this just helps to figur out the current settings... listening for the theme changed notification is a bit trickier. After your package is loaded, you must obtain an IVsShell instance via the DTE; once you have this object you can utilize the AdviceBroadcastMessages method to subscribe for event notifications. You have to provide an object whose type implements the IVsBroadcastMessageEvents interface...
I don´t want to post the whole implementation, but the following lines might illustrate the key scenario...
class VsBroadcastMessageEvents : IVsBroadcastMessageEvent
{
int IVsBroadcastMessageEvent.OnBroadcastMessage(uint msg, IntPtr wParam, IntPtr lParam)
{
const uint WM_SYSCOLORCHANGE = 0x15;
if (msg == WM_SYSCOLORCHANGE)
{
// obtain current theme from the Registry and update any UI...
}
}
}
Consider implementing IDisposable on that type as well, in order to be able to unsubscribe from the event source, when the package gets unloaded.
This is how I subscribe for event notifications...
class ShellService
{
private readonly IVsShell shell;
private bool advised;
public ShellService(IVsShell shellInstance)
{
this.shell = shellInstance;
}
public void AdviseBroadcastMessages(IVsBroadcastMessageEvents broadcastMessageEvents, out uint cookie)
{
cookie = 0;
try
{
int r = this.shell.AdviseBroadcastMessages(broadcastMessageEvents, out cookie);
this.advised = (r == VSConstants.S_OK);
}
catch (COMException) { }
catch (InvalidComObjectException) { }
}
public void UnadviseBroadcastMessages(uint cookie)
{
...
}
}
Keep the value of the cookie parameter; you´ll need it to successfully unsubscribe.
Hope that helps (-:
Just wanted to put an update just in case anyone else comes along.. #Matze and #Frank are totally right.. However in VS 2015.. they added a easy way to detect the theme change. So you need to include PlatformUI an dyou get a super easy event
using Microsoft.VisualStudio.PlatformUI;
....
//Then you get an event
VSColorTheme.ThemeChanged += VSColorTheme_ThemeChanged;
You should make sure your control is disposable so you can unsubscribe from the event...
BONUS!
It also give you easy access to the colors.. even if the user has changed them from the default .. so you can do stuff like this in when set your colors
var defaultBackground = VSColorTheme.GetThemedColor(EnvironmentColors.ToolWindowBackgroundColorKey);
var defaultForeground = VSColorTheme.GetThemedColor(EnvironmentColors.ToolWindowTextColorKey);
For VS 2015 this has changed, the solution #Matze has still works but you need to update the GetThemeId() function to check for the version and if it's 14.0 (VS2015) look in a different place in the registry. The way the value is stored has changed also, it's still a string but now contains other values seperated by a '*'. The theme guid is the last value in the list.
if (version == "14.0")
{
string keyName = string.Format(#"Software\Microsoft\VisualStudio\{0}\ApplicationPrivateSettings\Microsoft\VisualStudio", version);
using (RegistryKey key = Registry.CurrentUser.OpenSubKey(keyName))
{
if (key != null)
{
var keyText = (string)key.GetValue("ColorTheme", string.Empty);
if (!string.IsNullOrEmpty(keyText))
{
var keyTextValues = keyText.Split('*');
if (keyTextValues.Length > 2)
{
return keyTextValues[2];
}
}
}
}
return null;
}
Basically I have following architecture:
Website-project (1)
Domain-project (2)
Api-project (3)
Dependencies:
1 uses 2 and 3
3 uses 2
2 uses nothing
In my Api-project I define a concrete implementation of ServiceStack.Webhost.Endpoints.AppHostBase, eg ApiAppHost:
public sealed class ApiAppHost : AppHostBase
{
private ApiAppHost()
: base("Description", typeof (ApiAppHost).Assembly) {}
public override void Configure(Container container)
{
this.SetConfig(new EndpointHostConfig
{
ServiceStackHandlerFactoryPath = "api"
});
this.Routes.Add<Foo>("/foo", "POST");
}
public static void Initialize()
{
var instance = new ApiAppHost();
instance.Init();
}
}
This is pretty straight-forward.
Now I want to query my this.Routes (in combination with EndpointHostConfig.ServiceStackHandlerFactoryPath) from my Website-project to get the specific path for Foo.
How can I do that without creating an interceptor on my own? Does ServiceStack.Net provide anything which fits?
Currently I am doing something like this
public static class AppHostBaseExtensions
{
public static string GetUrl<TRequest>(this AppHostBase appHostBase)
{
var requestType = typeof (TRequest);
return appHostBase.GetUrl(requestType);
}
public static string GetUrl(this AppHostBase appHostBase, Type requestType)
{
var endpointHostConfig = appHostBase.Config;
var serviceStackHandlerFactoryPath = endpointHostConfig.ServiceStackHandlerFactoryPath;
var serviceRoutes = appHostBase.Routes as ServiceRoutes;
if (serviceRoutes == null)
{
throw new NotSupportedException("Property Routes of AppHostBase is not of type ServiceStack.ServiceHost.ServiceRoutes");
}
var restPaths = serviceRoutes.RestPaths;
var restPath = restPaths.FirstOrDefault(arg => arg.RequestType == requestType);
if (restPath == null)
{
return null;
}
var path = restPath.Path;
var virtualPath = "~/" + string.Concat(serviceStackHandlerFactoryPath, path); // bad, i know, but combining with 2 virtual paths ...
var absolutePath = VirtualPathUtility.ToAbsolute(virtualPath);
return absolutePath;
}
}
I know that it is wrong because of many issues (path-combining, not taking rest-paths into account, not taking rest-paths with placeholders into account), but it works as a start ...
Edit:
And this only works, if you register the route within Configure(Container) of your AppHostBase-implementation. It wont't work with the RestServiceAttribute-attribute ...
I'm trying to write a target for NLog to send messages out to connected clients using SignalR.
Here's what I have now. What I'm wondering is should I be using resolving the ConnectionManager like this -or- somehow obtain a reference to the hub (SignalrTargetHub) and call a SendMessage method on it?
Are there performance ramifications for either?
[Target("Signalr")]
public class SignalrTarget:TargetWithLayout
{
public SignalR.IConnectionManager ConnectionManager { get; set; }
public SignalrTarget()
{
ConnectionManager = AspNetHost.DependencyResolver.Resolve<IConnectionManager>();
}
protected override void Write(NLog.LogEventInfo logEvent)
{
dynamic clients = GetClients();
var logEventObject = new
{
Message = this.Layout.Render(logEvent),
Level = logEvent.Level.Name,
TimeStamp = logEvent.TimeStamp.ToString("yyyy-MM-dd HH:mm:ss.fff")
};
clients.onLoggedEvent(logEventObject);
}
private dynamic GetClients()
{
return ConnectionManager.GetClients<SignalrTargetHub>();
}
}
I ended up with the basic the same basic structure that I started with. Just a few tweaks to get the information I needed.
Added exception details.
Html encoded the final message.
[Target("Signalr")]
public class SignalrTarget:TargetWithLayout
{
protected override void Write(NLog.LogEventInfo logEvent)
{
var sb = new System.Text.StringBuilder();
sb.Append(this.Layout.Render(logEvent));
if (logEvent.Exception != null)
sb.AppendLine().Append(logEvent.Exception.ToString());
var message = HttpUtility.HtmlEncode(sb.ToString());
var logEventObject = new
{
Message = message,
Logger = logEvent.LoggerName,
Level = logEvent.Level.Name,
TimeStamp = logEvent.TimeStamp.ToString("HH:mm:ss.fff")
};
GetClients().onLoggedEvent(logEventObject);
}
private dynamic GetClients()
{
return AspNetHost.DependencyResolver.Resolve<IConnectionManager>().GetClients<SignalrTargetHub>();
}
}
In my simple testing it's working well. Still remains to be seen if this adds any significant load when under stress.