I would like to get some advice. I am developing a system that will load up plugins at runtime and require them to be available through a WCF endpoint.
I will have a MVC 3 Web app that is only really used for configuration, and a class library (core) that will load up different plugins.
I would appreciate some guidance on how to go about this. I would like to load the plugin up and then be able to create a WCF endpoint that is registered with IIS 7 for access into that plugin.
Thanks in advance :)
Using a derivative of Darko's Dynamic IIS hosted WCF Service work, you can achieve something what you want. Let's start with an example service we might want to host, we'll call it an IMessageBroker, it's contract is simple:
[ServiceContract]
public interface IMessageBroker
{
[OperationContract]
string Send(string message);
}
We use this contract for both the Service, and the MEF Exports/Imports. We'll also define some additional metadata to go along with it:
public interface IMessageBrokerMetadata
{
public string Name { get; }
public string Channel { get; }
}
As it's a simple project, I'll cheat and use a simple static class for managing the MEF CompositionContainer used to compose parts:
public static class MEF
{
private static CompositionContainer container;
private static bool initialised;
public static void Initialise()
{
var catalog = new DirectoryCatalog("bin");
container = new CompositionContainer(catalog);
initialised = true;
}
public static CompositionContainer Container
{
get
{
if (!initialised) Initialise();
return container;
}
}
}
To be able to generate WCF Services dynamically, we need to create a ServiceHostFactory that can access our composition container to access our types, so you could do:
public class MEFServiceHostFactory : ServiceHostFactory
{
public override ServiceHostBase CreateServiceHost(string constructorString, System.Uri[] baseAddresses)
{
var serviceType = MEF.Container
.GetExports<IMessageBroker, IMessageBrokerMetadata>()
.Where(l => l.Metadata.Name == constructorString)
.Select(l => l.Value.GetType())
.Single();
var host = new ServiceHost(serviceType, baseAddresses);
foreach (var contract in serviceType.GetInterfaces())
{
var attr = contract.GetCustomAttributes(typeof(ServiceContractAttribute), true).FirstOrDefault();
if (attr != null)
host.AddServiceEndpoint(contract, new BasicHttpBinding(), "");
}
var metadata = host.Description.Behaviors
.OfType<ServiceMetadataBehavior>()
.FirstOrDefault();
if (metadata == null)
{
metadata = new ServiceMetadataBehavior();
metadata.HttpGetEnabled = true;
host.Description.Behaviors.Add(metadata);
}
else
{
metadata.HttpGetEnabled = true;
}
return host;
}
}
Essentially the constructorString argument is used to pass in the Metadata name we want for the specific service. Next up, we need to handle locating these services. What we now need is a VirtualPathProvider which we can use to dynamically create the instance, through a VirtualFile. The provider would look like:
public class ServiceVirtualPathProvider : VirtualPathProvider
{
private bool IsServiceCall(string virtualPath)
{
virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
return (virtualPath.ToLower().StartsWith("~/services/"));
}
public override VirtualFile GetFile(string virtualPath)
{
return IsServiceCall(virtualPath)
? new ServiceFile(virtualPath)
: Previous.GetFile(virtualPath);
}
public override bool FileExists(string virtualPath)
{
if (IsServiceCall(virtualPath))
return true;
return Previous.FileExists(virtualPath);
}
public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
{
return IsServiceCall(virtualPath)
? null
: Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
What we are doing, is mapping any calls to /Services/ to our MEF derived endpoints. The service needs a virtual file, and this is where we tie it all together:
public class ServiceFile : VirtualFile
{
public ServiceFile(string virtualPath) : base(virtualPath)
{
}
public string GetName(string virtualPath)
{
string filename = virtualPath.Substring(virtualPath.LastIndexOf("/") + 1);
filename = filename.Substring(0, filename.LastIndexOf("."));
return filename;
}
public override Stream Open()
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write("<%# ServiceHost Language=\"C#\" Debug=\"true\" Service=\"" + GetName(VirtualPath) +
"\" Factory=\"Core.MEFServiceHostFactory, Core\" %>");
writer.Flush();
stream.Position = 0;
return stream;
}
}
The virtual file will break out the Metadata name from the virtual path, where /Services/SampleMessageBroker.svc -> SampleMessageBroker. We then generate some markup which represents the markup of an .svc file with Service="SampleMessageBroker". This argument will be passed to the MEFServiceHostFactory where we can select out endpoints. So, given a sample endpoint:
[Export(typeof(IMessageBroker)),
ExportMetadata("Name", "SampleMessageBroker"),
ExportMetadata("Channel", "Greetings")]
public class SampleMessageBroker : IMessagerBroker
{
public string Send(string message)
{
return "Hello! " + message;
}
}
We can now access that dynamically at /Services/SampleMessageBroker.svc. What you might want to do, is provide a static service which allows you to interegate what endpoints are available, and feed that back to your consuming clients.
Oh, don't forget to wire up your virtual path provider:
HostingEnvironment.RegisterVirtualPathProvider(new ServiceVirtualPathProvider());
Related
I got stuck and need some advice or pointer to a solution.
A web API with ASP.NET Core 3.1
Startup.cs
services.AddSingleton<ITopicClient>(s => new TopicClient({connectionstring},{topic}));
TopicRepository.cs
public class TopicRepository : ITopicRepository
{
private readonly ITopicClient _topicClient1;
private readonly ITopicClient _topicClient2;
public TopicRepository (ITopicClient topicClient1, ITopicClient topicClient2)
{
_topicClient1 = topicClient1;
_topicClient2 = topicClient2;
}
public async Task<Response> SendToTopicAsync(string message, string topic)
{
if( topic == "topic1")
await _topicClient1.send(message);
else if (topic == "topic2")
await _topicClient2.send(message);
}
}
TopicClient.cs in a shared library
public TopicClient(string serviceBusConnectionString, string topicName)
{
_topicClient = new TopicClient(_serviceBusConnectionString,topicName);
}
I need to send message to different topics. I would like to register services with different topic names in startup.cs. I want to reuse topicClient connection.
services.AddSingleton(s => new TopicClient({connectionstring},{topic1}));
services.AddSingleton(s => new TopicClient({connectionstring},{topic2}));
How can I achieve this by registering singleton instances of same type using same interface ?
Thank you in advance!
You could use a client resolver that holds your registered clients with a wrapper around the client.
First create a wrapper around your client with a name or enum for how to resolve it. As I'm not a fan of magic strings I decided to go with an enum in the example.
// Wrapper for your TopicClients
public interface ICustomTopicClient
{
public ITopicClient TopicClient { get; }
public TopicName TopicName { get; }
}
// Implement the ICustomTopicClient interface
public class CustomTopicClient : ICustomTopicClient
{
public ITopicClient TopicClient { get; }
public TopicName TopicName { get; }
public CustomTopicClient(ITopicClient topicClient, TopicName topicName)
{
TopicClient = topicClient;
TopicName = topicName;
}
}
// Enum for how to resolve the requested TopicClient
public enum TopicName
{
Topic1 = 0,
Topic2 = 1
}
// Register all ICustomTopicClients in your container
services.AddSingleton<ICustomTopicClient>(s => new CustomTopicClient(new TopicClient({connectionstring},{topic}), TopicName.Topic1));
services.AddSingleton<ICustomTopicClient>(s => new CustomTopicClient(new TopicClient({connectionstring},{topic2}), TopicName.Topic2));
Then you create a resolver that holds all custom clients.
You inject the collection of clients from the container and create a dictionary with a public method to resolve the clients.
public interface IMessageClientResolver
{
ITopicClient ResolveClient(TopicName name);
}
public class MessageClientResolver : IMessageClientResolver
{
private readonly Dictionary<TopicName, ITopicClient> topicClients;
public MessageClientResolver(IEnumerable<ICustomTopicClient> clients)
{
topicClients = clients.ToDictionary(k => k.TopicName, v => v.TopicClient);
}
public ITopicClient ResolveClient(TopicName name)
{
topicClients.TryGetValue(name, out var client);
if (client is null)
throw new ArgumentException(nameof(client));
return client;
}
}
Register the resolver to the container.
services.AddSingleton<IMessageClientResolver, MessageClientResolver>();
And then use it like this:
public class Foo
{
private readonly ITopicClient topicClient;
private readonly ITopicClient topicClient2;
public Foo(IMessageClientResolver clientResolver)
{
topicClient = clientResolver.ResolveClient(TopicName.Topic1);
topicClient2 = clientResolver.ResolveClient(TopicName.Topic2);
}
}
You can use the same pattern and extend the resolver with IQueueClients. And add a resolve method to return the IQueueClient by a QueueName enum.
You can already register multiple instances as the same interface, so when you do:
services.AddSingleton<ITopicClient>(_ => new TopicClient("topic1"));
services.AddSingleton<ITopicClient>(_ => new TopicClient("topic2"));
you already added two instances to the container.
It is just when you resolve interface ITopicClient, you always get the last added instance. For example, if you resolve:
// instance = topic2
var instance = container.GetService<ITopicClient>();
If you need all instances, you should resolve / inject IEnumerable<ITopicClient>.
class TopicRepository
{
public TopicRepository(IEnumerable<ITopicClient> clients)
{
// clients contains topic1 and topic2
}
}
For MVC app using MEF, sometimes I get the erorr ""
I have a .NET solution with
One MVC Web application project and,
Many class libraries projects which take care of authentication, getting configurations, making external API calls etc.
I have configured the MEF as shown below through the code and deployed this on Web Server using IIS. Observed the below error couple of times, after which I tried to load the page multiple times but it still throws the same error.
Once I refreshed the application pool, then only the error disappears. I had been trying hard to debug and understand the error, but with no success. Am I misconfiguring MEF anywhere?
Global.asax:
public class Global : HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
RouteConfig.RegisterRoutes(RouteTable.Routes);
var pluginFolders = LoadMefComponents();
Bootstrapper.Compose(pluginFolders);
IControllerFactory mefControllerFactory = new MefControllerFactory(Bootstrapper.Container);
ControllerBuilder.Current.SetControllerFactory(mefControllerFactory);
}
protected List<string> LoadMefComponents()
{
var pluginFolders = new List<string>();
string ModulesPath = CommonUtility.GetApplicationDirectory();
var plugins = Directory.GetDirectories(ModulesPath).ToList();
plugins.ForEach(path =>
{
var directoryInfo = new DirectoryInfo(path);
pluginFolders.Add(directoryInfo.Name);
});
return pluginFolders;
}
}
MEFControllerFactory.cs: This file resides in App_Start
public class MefControllerFactory : DefaultControllerFactory
{
private readonly CompositionContainer _container;
private readonly Dictionary<IController, Lazy<object, object>> exports;
private readonly object syncRoot;
public MefControllerFactory(CompositionContainer container)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
this._container = container;
this.exports = new Dictionary<IController, Lazy<object, object>>();
this.syncRoot = new object();
}
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
Lazy<object, object> export = _container.GetExports(controllerType, null, null).FirstOrDefault();
var controller = null == export ? base.GetControllerInstance(requestContext, controllerType)
: (IController)export.Value;
lock (this.syncRoot)
{
this.exports.Add(controller, export);
}
return controller;
}
public override void ReleaseController(IController controller)
{
lock (this.syncRoot)
{
var export = this.exports[controller];
this.exports.Remove(controller);
// this._container.ReleaseExport(export);
}
((IDisposable)controller).Dispose();
}
}
Bootstrapper.cs: This file resides in App_Start
public class Bootstrapper
{
private static CompositionContainer compositionContainer;
private static bool IsLoaded = false;
public static CompositionContainer Container
{
get { return compositionContainer; }
set { compositionContainer = value; }
}
public static void Compose(List<string> pluginFolders)
{
if (IsLoaded) return;
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
catalog.Catalogs.Add(new DirectoryCatalog(CommonUtility.GetApplicationDirectory()));
compositionContainer = new CompositionContainer(catalog);
compositionContainer.ComposeParts();
IsLoaded = true;
}
public static T GetInstance<T>(string contractName = null)
{
var type = default(T);
if (compositionContainer == null) return type;
if (!string.IsNullOrWhiteSpace(contractName))
type = compositionContainer.GetExportedValue<T>(contractName);
else
type = compositionContainer.GetExportedValue<T>();
return type;
}
}
CommonUtility.cs: This file resides in App_Start
public class CommonUtility
{
public static string GetApplicationDirectory()
{
string codeBase = Assembly.GetExecutingAssembly().CodeBase;
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
return Path.GetDirectoryName(path);
}
}
HomeController.cs:
[CommonExceptionFilter]
public class HomeController : Controller
{
private IConfigurationManager _configurationManager;
[ImportingConstructor]
public HomeController()
{
_configurationManager = Bootstrapper.GetInstance<IConfigurationManager>();
}
public async Task<ActionResult> Index()
{
//Business Logic
return View()
}
}
IConfigurationManager.cs
[InheritedExport]
public interface IConfigurationManager
{
string GetConfigurationValue(string keyName)
}
ConfigurationManager.cs
[PartCreationPolicy(CreationPolicy.Shared)]
public class ConfigurationManager: IConfigurationManager
{
[ImportingConstructor]
public ConfigurationManager()
{
}
public string GetConfigurationValue(string keyName)
{
return "";
}
}
IHttpHandlers Used In The MVC Project:
public class CommonServiceHandler : HttpTaskAsyncHandler, IRequiresSessionState
{
private ICommonServiceHandlerManager _commonServiceHandlerManager;
public CommonServiceHandler()
{
_commonServiceHandlerManager = Bootstrapper.GetInstance<ICommonServiceHandlerManager>();
}
public override bool IsReusable
{
get
{
return false;
}
}
}
This error indicates that at least one controller exists that its constructor parameters are not resolved.
Every controller needs a constructor to be resolved at run-time. By default every c# class has a default (parameter-less) constructor that can be called when an instance of the class is needed to be created.
However, after defining an explicit constructor, you lose the default constructor, therefore, you need to make sure that all your controllers have either a parameter-less constructor or if instead they have a parametric constructor the parameters need to be registered through dependency injection.
This one is a follow up question to Dependency Injection using Unity
So , as a set up I have a CustomConfiguration.cs file which is supposed to populate from a config section in my web.config file
This is the signature for the file
public class CustomConfiguration : ICustomProcessorConfig, IEmailConfig, IReportConfig
{
#region Properties
private CustomProcessorConfig ConfigSection { get; set; }
#endregion
#region Constructors (1)
public CustomConfiguration()
{
ConfigSection = ConfigurationManager.GetSection("customConfiguration") as ConfigSection;
}
#endregion Constructors
#region ICustomConfiguration Members
public string Text { get { return ConfigSection.Text; } }
public string Subject { get { return ConfigSection.Subject; } }
public string SmtpHost { get { return ConfigSection.EmailSettings.SmtpHost; } }
public int SmtpPort { get { return ConfigSection.EmailSettings.SmtpPort; } }
These are the 3 files I have for Email Generation :
public interface IEmailManager
{
void SendEmail(string toAddress, string fromAddress, string subject, string body, bool htmlBody);
}
public interface IEmailConfig
{
string SmtpHost { get; }
int SmtpPort { get; }
}
And Finally I have the Email Manager which inherits the IEmailManager interface
public class EmailManager: IEmailManager
{
#region Constructors (1)
public EmailManager(IEmailConfiguration configuration)
{
CurrentSmtpClient = new SmtpClient
{
Host = configuration.SmtpHost,
Port = configuration.SmtpPort,
Credentials =
new NetworkCredential(configuration.UserName, configuration.Password)
};
}
#endregion Constructors
// send Mail is also implemented
}
Coming back to the previous question I have my set up like :
Container = new UnityContainer();
Container.RegisterType<ICustomConfiguration,CustomConfiguration>(new ContainerControlledLifetimeManager());
Container.RegisterType<IEmailManager, EmailManager>(new ContainerControlledLifetimeManager());
Container.RegisterType<IReportGenerator, ReportGenerator>(new ContainerControlledLifetimeManager());
Configuration = Container.Resolve<ICustomConfiguration>();
Emailer = Container.Resolve<IEmailManager>();
ReportGenerator = Container.Resolve<IReportGenerator>();
I'm getting a ResolutionFailedExceptionsaying The parameter configuration could not be resolved when attempting to call constructor for EmailManager.
I had a different DI setup and I would need the configuration information from IEmailConfig still. Is there a way of going past this ? I need the config information to proceed with sending the email as you can guess with my setup.
Am I binding different repo to my Container ? Or how should I change my EmailManager code ?
You need to register the IEmailConfig interface with the CustomConfiguration class in the container.
Container.RegisterType<IEmailConfig , CustomConfiguration >(new ContainerControlledLifetimeManager());
IEmailConfiguration missing mapping in the unity container. Need to add the concrete class that maps this interface
First off, let me say that I don't think that is is an issue with ReactiveUI per se, which is why I've not created an issue on its github repo, and second, I realise that I'm using a beta version of ReactiveUI.
I want to use Structuremap because I'm going to have a plugin scenario in my WPF app, and the DI container in Splat isn't cut out for that sort of thing.
Observe these unit tests:
[Fact]
public void ShouldBeAbleToOverrideDefaultDependencyResolver()
{
Locator.Current = new ApplicationDependencyResolver(StructureMapBootstrapper.Instance.Container);
Locator.CurrentMutable.InitializeSplat();
Locator.CurrentMutable.InitializeReactiveUI();
var view = Locator.Current.GetService<SplashScreenView>();
view.Should().NotBeNull().And.BeOfType<SplashScreenView>();
}
[Fact]
public void ShouldBeAbleToLocateTheViewForAViewModel()
{
Locator.Current = new ApplicationDependencyResolver(StructureMapBootstrapper.Instance.Container);
Locator.CurrentMutable.InitializeSplat();
Locator.CurrentMutable.InitializeReactiveUI();
var viewLocator = Locator.Current.GetService<IViewLocator>();
var view = viewLocator.ResolveView(typeof (SplashScreenViewModel));
view.Should().NotBeNull().And.BeOfType<SplashScreenView>();
}
The first test passes. The second test does not, and provides this stacktrace:
StructureMap.StructureMapConfigurationExceptionNo default Instance is registered and cannot be automatically determined for type 'IViewFor<RuntimeType>'
There is no configuration specified for IViewFor<RuntimeType>
1.) Container.GetInstance(IViewFor<RuntimeType>)
at StructureMap.SessionCache.GetDefault(Type pluginType, IPipelineGraph pipelineGraph) in c:\BuildAgent\work\996e173a8ceccdca\src\StructureMap\SessionCache.cs: line 63
at StructureMap.Container.GetInstance(Type pluginType) in c:\BuildAgent\work\996e173a8ceccdca\src\StructureMap\Container.cs: line 325
at Redacted.ApplicationDependencyResolver.GetService(Type serviceType, String contract) in ApplicationDependencyResolver.cs: line 27
at ReactiveUI.DefaultViewLocator.attemptToResolveView(Type type, String contract)
at ReactiveUI.DefaultViewLocator.ResolveView(T viewModel, String contract)
at Redacted.BootstrapAndDependencyResolutionTests.ShouldBeAbleToLocateTheViewForAViewModel() in BootstrapAndDependencyResolutionTests.cs: line 39
I obviously do not, and can not, have any views which implement IViewFor<RuntimeType>. Anyone got any ideas as to why this is happening, and what I can do to get around this? I can't exclude it using the normal Structuremap configuration.
For full clarity here are the implementations of the resolver and the structuremap bootstrapper:
public class ApplicationDependencyResolver : IMutableDependencyResolver
{
private readonly IContainer _container;
public ApplicationDependencyResolver(IContainer container)
{
_container = container;
}
public void Dispose()
{
_container.Dispose();
}
public object GetService(Type serviceType, string contract = null)
{
return string.IsNullOrEmpty(contract)
? _container.GetInstance(serviceType)
: _container.GetInstance(serviceType, contract);
}
public IEnumerable<object> GetServices(Type serviceType, string contract = null)
{
return _container.GetAllInstances(serviceType).Cast<object>();
}
public void Register(Func<object> factory, Type serviceType, string contract = null)
{
var o = factory();
_container.Configure(configure => configure.For(serviceType).Use(o));
}
}
public sealed class StructureMapBootstrapper
{
private static readonly StructureMapBootstrapper InternalInstance = new StructureMapBootstrapper();
static StructureMapBootstrapper() { }
private StructureMapBootstrapper()
{
Configure();
}
public static StructureMapBootstrapper Instance { get { return InternalInstance; } }
public IContainer Container { get; private set; }
private void Configure()
{
Container = new Container(configure =>
{
configure.Scan(with =>
{
with.TheCallingAssembly();
with.LookForRegistries();
with.WithDefaultConventions();
});
});
}
}
After some quality time with the ReactiveUI unit tests, it turns out that the unit test which was failing was actually not implemented correctly, and should look like this:
[Fact]
public void ShouldBeAbleToLocateTheViewForAViewModel()
{
var container = StructureMapBootstrapper.Instance.Container;
var ihas = container.WhatDoIHave();
Locator.Current = new ApplicationDependencyResolver(container);
Locator.CurrentMutable.InitializeSplat();
Locator.CurrentMutable.InitializeReactiveUI();
var vm = new SplashScreenViewModel();
var viewLocator = Locator.Current.GetService<IViewLocator>();
var view = viewLocator.ResolveView(vm);
view.Should().NotBeNull().And.BeOfType<SplashScreenView>();
}
Specifically, it was the fact I was passing typeof(SplashScreenViewMode), and not an instance, that was causing the test to fail.
Edit: I also had to add with.AddAllTypesOf(typeof (IViewFor<>)); to the Structuremap configuration.
I'm trying to hook Fluent Validation to my MVC WEB Api project, and it doesn't wanna work.
When I use MyController : Controller -> works fine (ModelState.IsValid returns False)
but when I use MyController :ApiController ... nothing.
Does anyone have experience on how to hook those up ?
latest version of Fluent Validation (5.0.0.1) supports web api
Just install it from Nuget and register it in Global.asax like so:
using FluentValidation.Mvc.WebApi;
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
...
FluentValidationModelValidatorProvider.Configure();
}
}
The answer is in this pull request.
Basically You need to implement custom ModelValidation Provider.
And a couple more things to note:
Web API don't work with modelValidator from System.Web.Mvc namespace, only with the ones from System.Web.Http as noted here:
Server side validation with custom DataAnnotationsModelValidatorProvider
You don't add it like this:
ModelValidatorProviders.Providers.Add(new WebApiFluentValidationModelValidatorProvider());`
BUT like this:
GlobalConfiguration.Configuration.Services.Add(typeof(System.Web.Http.Validation.ModelValidatorProvider), new WebApiFluentValidationModelValidatorProvider());`
I have found another simple solution for using FluentValidation in Web API, but it lacks integration with ModelState and Metadata. However, when building an API that doesn't need to return the entire ModelState to the client (as is needed in MVC to rebuild the page), I have found the trade-off for simplicity to be worthwhile. Whenever an API input is invalid, I return a 400 Bad Request status code with a list of property IDs and error messages. To do this, I use a simple ActionFilterAttribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateInputsAttribute : ActionFilterAttribute
{
private static readonly IValidatorFactory ValidatorFactory = new AttributedValidatorFactory();
public override void OnActionExecuting(HttpActionContext actionContext)
{
base.OnActionExecuting(actionContext);
var errors = new Dictionary<string, string>();
foreach (KeyValuePair<string, object> arg in actionContext.ActionArguments.Where(a => a.Value != null))
{
var argType = arg.Value.GetType();
IValidator validator = ValidatorFactory.GetValidator(argType);
if (validator != null)
{
var validationResult = validator.Validate(arg.Value);
foreach (ValidationFailure error in validationResult.Errors)
{
errors[error.PropertyName] = error.ErrorMessage;
}
}
}
if (errors.Any())
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
}
}
}
This attribute can be added as a global filter, to individual controllers/actions, or to a base class.
This code can certainly be improved, but it has served me well so far so I wanted to make it available to others. Here are some of its shortcomings:
Null inputs are not validated. I thought that this would be more of a problem, but in practice it simply doesn't happen much (if at all) in our app. My controllers throw ArgumentNullExceptions for null inputs which would return a 500 to the client informing the client that the input cannot be null.
I can't use ModelState in my controllers. But, after validating the required inputs are non-null, I already know that the ModelState is valid so this may actually serve to simplify code. But it's important for devs to know not to use it.
Right now this implementation is hard coded for the AttributedValidatorFactory. This should be abstracted, but it's been pretty low on my priority list so far.
As I was looking to solve this I wanted to make it so that the same validator instance could be used for MVC and Web API. I was able to accomplish this by making two factories and using them together.
MVC Factory:
public class MVCValidationFactory : ValidatorFactoryBase
{
private readonly IKernel _kernel;
public MVCValidationFactory(IKernel kernel)
{
_kernel = kernel;
}
public override IValidator CreateInstance(Type validatorType)
{
var returnType = _kernel.TryGet(validatorType);
return returnType as IValidator;
}
}
API Factory:
public class WebAPIValidationFactory : ModelValidatorProvider
{
private readonly MVCValidationFactory _mvcValidationFactory;
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public WebAPIValidationFactory(MVCValidationFactory mvcValidationFactory)
{
_mvcValidationFactory = mvcValidationFactory;
}
public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
{
try
{
var type = GetType(metadata);
if (type != null)
{
var fluentValidator =
_mvcValidationFactory.CreateInstance(typeof(FluentValidation.IValidator<>).MakeGenericType(type));
if (fluentValidator != null)
{
yield return new FluentValidationModelValidator(validatorProviders, fluentValidator);
}
}
}
catch (Exception ex)
{
Log.Error(ex);
}
return new List<ModelValidator>();
}
private static Type GetType(ModelMetadata metadata)
{
return metadata.ContainerType != null ? metadata.ContainerType.UnderlyingSystemType : null;
}
The trick then was figuring out how to run the validation for both MVC and Web API. I ended up creating a wrapper for the IValidator<> that worked with the ModelValidator signature.
public class FluentValidationModelValidator : ModelValidator
{
public IValidator innerValidator { get; private set; }
public FluentValidationModelValidator(
IEnumerable<ModelValidatorProvider> validatorProviders, IValidator validator)
: base(validatorProviders)
{
innerValidator = validator;
}
public override IEnumerable<ModelValidationResult> Validate(ModelMetadata metadata, object container)
{
if (InnerValidator != null && container != null)
{
var result = innerValidator.Validate(container);
return GetResults(result);
}
return new List<ModelValidationResult>();
}
private static IEnumerable<ModelValidationResult> GetResults(FluentValidation.Results.ValidationResult result)
{
return result.Errors.Select(error =>
new ModelValidationResult
{
MemberName = error.PropertyName,
Message = error.ErrorMessage
}));
}
}
The last part was to wire up the validators in the Global.asax:
MVCValidationFactory mvcValidationFactory = new MVCValidationFactory(KernelProvider.Instance.GetKernel());
GlobalConfiguration.Configuration.Services.Add(
typeof(ModelValidatorProvider),
new WebAPIValidationFactory(mvcValidationFactory));
ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(mvcValidationFactory));
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
Sorry this was a bit long, but hopefully it helps someone out.
In the WebApiConfig add two lines
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// snip...
//Fluent Validation
config.Filters.Add(new ValidateModelStateFilter());
FluentValidationModelValidatorProvider.Configure(config);
}
}
Create a model and a validator as follows -
[Validator(typeof(PersonCreateRequestModelValidator))]
public class PersonCreateRequestModel
{
public Guid PersonId { get; set; }
public string Firstname { get; set; }
public string Lastname { get; set; }
}
public class PersonCreateRequestModelValidator : AbstractValidator
{
//Simple validator that checks for values in Firstname and Lastname
public PersonCreateRequestModelValidator()
{
RuleFor(r => r.Firstname).NotEmpty();
RuleFor(r => r.Lastname).NotEmpty();
}
}
That's about all you need. Just write the controller as you would normally.
public IHttpActionResult Post([FromBody]PersonCreateRequestModel requestModel)
{
//snip..
//return Ok(some new id);
}
If you want a full source code example you can get it here - http://NoDogmaBlog.bryanhogan.net/2016/12/fluent-validation-with-web-api-2/
Latest version of Fluent Validation doesn't support Mvc 4 or Web Api.
Read this.