I'm trying to introduce automapper in such a way that for WebAPI all DTOs are transparent, so basically I want to map objects in runtime and convert them to Domain objects before they reach controller method.
I have automapper filter attribute
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class AutoMapAttribute : ActionFilterAttribute
{
private readonly Type _sourceType;
private readonly Type _destType;
public AutoMapAttribute(Type sourceType, Type destType)
{
_sourceType = sourceType;
_destType = destType;
}
#region Overrides of ActionFilterAttribute
/// <summary>Occurs before the action method is invoked.</summary>
/// <param name="actionContext">The action context.</param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
var mapper = ServiceLocator.Locator.GetInstance<IMapper>();
var jsonObject = actionContext.ActionArguments.FirstOrDefault(x => x.Value.GetType() == typeof(Newtonsoft.Json.Linq.JObject));
var dto = JsonConvert.DeserializeObject(jsonObject.Value.ToString(), _sourceType);
object model = mapper.Map(dto, _sourceType, _destType);
actionContext.ActionArguments[jsonObject.Key] = model;
base.OnActionExecuting(actionContext);
}
#endregion
#region Overrides of ActionFilterAttribute
/// <summary>Occurs after the action method is invoked.</summary>
/// <param name="actionExecutedContext">The action executed context.</param>
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
}
#endregion
public Type SourceType
{
get { return _sourceType; }
}
public Type DestType
{
get { return _destType; }
}
}
and controller method:
[HttpPost]
[Route("")]
[AutoMap(typeof(Public.Dto.Product), typeof(Domain.Model.Product))]
public IHttpActionResult Post(object product)
{
_productBusinessLogic.Create(product);
return Ok();
}
and it works quite well as product variable in controlelr method is actually my Domain product. Now I want to change object type to concrete Domain.Product type in controller method definition. Unfortunately WebAPI tries to immediately deserialize object that comes from Request to this object type which breaks the whole idea.
I am able to use OWIN if it helps.
The key point is that the handling of action filters takes place after model binding in the Web API request stack.
Using OWIN we could read the original body as the source type and then map it to the target type:
Override the OnActionExecutingAsync method:
public override async Task OnActionExecutingAsync(HttpActionContext actionContext,
CancellationToken cancellationToken)
{
var argumentKey = actionContext.ActionArguments.FirstOrDefault().Key;
var owinContext = (OwinContext) actionContext.Request.Properties["MS_OwinContext"];
owinContext.Request.Body.Seek(0, SeekOrigin.Begin);
var source = await actionContext.Request.Content.ReadAsAsync(_sourceType, cancellationToken);
actionContext.ActionArguments[argumentKey] = Mapper.Map(source, _sourceType, _destType);
await base.OnActionExecutingAsync(actionContext, cancellationToken);
}
To reread the request's content in the action filter we need to add some handler to the pipeline in the OWIN Startup class:
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
app.Use(async (context, next) =>
{
var content = context.Request.Body;
if (content == Stream.Null)
{
await next();
}
else
{
using (var stream = new MemoryStream())
{
await content.CopyToAsync(stream);
stream.Position = 0;
context.Request.Body = stream;
await next();
}
context.Request.Body = content;
}
});
app.UseWebApi(config);
}
}
Related
There is an option to add new OutputFormatters globally for all controllers, but how to add OutputFormatters for selected Action?
builder.Services.AddControllers(options =>
{
options.OutputFormatters.Insert(0, new CsvOutputFormatter(new CsvFormatterOptions { CsvDelimiter = "," }));
})
I have found IResultFilter and it has the method OnResultExecuting where formatted can be added. After that, I can decorate the Action with a new ResultFilter attribute. Is this the correct way to assign custom formats for certain actions?
public class CsvResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is ObjectResult objectResult)
{
var csvOutputFormatter = new CsvOutputFormatter(new CsvFormatterOptions { CsvDelimiter = "," });
objectResult.Formatters.Add(csvOutputFormatter);
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
Another problem I am facing is if I decorate Action with attribute I get an exception that the service is not registered.
[ServiceFilter(typeof(CsvResultFilter))]
public async Task<ActionResult> Statistic([FromQuery] ExportStatisticParams model, CancellationToken cancellationToken)
{
}
If I register it in builder.Services.AddControllers(options => options.Filters.Add(typeof(CsvResultFilter))) the CsvResultFilter filter gets executed on all actions.
There's a couple of things you need to do to make this work. If you really need to use ServiceFilterAttribute then you need to ensure:
The output formatter type registered in the DI container (e.g. builder.Services.AddSingleton<CsvOutputFormatter>();
The formatter must also implement IFilterMetadata.
However, there's another way which I think is a little cleaner. You can create your own attribute that inherits from ActionFilterAttribute. Something like this:
public class CsvOutputAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is ObjectResult objectResult)
{
// You could also use DI here with
// context.HttpContext.RequestServices.GetService<CsvOutputFormatter>();
var formatter = new CsvOutputFormatter(...);
//objectResult.Formatters.Clear(); //<- You may want to add this?
objectResult.Formatters.Add(formatter);
}
}
}
And decorate your action or controller with it:
[CsvOutput]
public IActionResult MyAction()
{
return new ObjectResult("hello");
}
Normally, I'd just do in my controller action:
return Content(System.Net.HttpStatusCode.InternalServerError,
new MyCustomObject("An error has occurred processing your request.", // Custom object, serialised to JSON automatically by the web api service
ex.ToString()));`
However the Content method exists on the controller. The ExceptionHandler I made has this:
public override void Handle(ExceptionHandlerContext context)
{
context.Result = ???;
The type of context.Result is IHttpActionResult, so what I need to do is create one and stick it in there. I can't find any constructors or similar that will allow me to create an IHttpActionResult outside of a controller. Is there an easy way?
I thing for custom responses you should probably implement your own http action result:
public override void Handle(ExceptionHandlerContext context)
{
context.Result = new HttpContentResult(new { }, context.Request);
}
public class HttpContentResult : IHttpActionResult
{
private readonly object content;
private readonly HttpRequestMessage requestMessage;
public HttpContentResult(object content, HttpRequestMessage requestMessage)
{
this.content = content;
this.requestMessage = requestMessage;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var httpContentResponse = new HttpResponseMessage(HttpStatusCode.BadRequest);
var httpContent = new StringContent(content);
//... [customize http contetnt properties]
httpContentResponse.Content = httpContent;
httpContentResponse.RequestMessage = this.requestMessage;
//... [customize another http response properties]
return Task.FromResult(httpContentResponse);
}
}
I created an action filter to check if the id is correct according to the incoming id.
The constructor method of the filter takes a service type parameter.
I need service to check this id before ActionMethod runs.But GetService() method returns null.
ActionFilter
public class ContainsFilterAttribute : ActionFilterAttribute
{
private Type _service;
private Type _entity;
private IdSections _idSections;
public ContainsFilterAttribute(Type service, Type entity, IdSections idSections)
{
if (!typeof(ICommonService).IsAssignableFrom(service) || !typeof(IEntity).IsAssignableFrom(entity))
{
throw new System.Exception("Service or entity undefined.");
}
_service = service;
_entity = entity;
_idSections = idSections;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
//returns null service
var service = (ICommonService)context.HttpContext.RequestServices.GetService(_service);
var entity = (IEntity)Activator.CreateInstance(_entity);
if (IdSections.FromRoute == _idSections)
{
entity.Id = (int)context.ActionArguments.FirstOrDefault(pair => pair.Key.ToUpper().Contains("ID")).Value;
}
var res = service.Contains(entity);
}
}
ActionMethod
[HttpGet]
[Route("{userId}/user-channels")]
[ContainsFilter(typeof(UserManager), typeof(User), IdSections.FromRoute)]
public IActionResult GetUserChannels(int userId)
{
var result = _channelService.GetUserChannels(userId);
if (result.IsSuccessful)
{
var mapResult = _mapper.Map<List<ChannelForListDto>>(result.Data);
return Ok(mapResult);
}
return this.ServerError(result.Message);
}
Injection
services.AddScoped<IUserService, UserManager>();
IUserService is registered as the service
services.AddScoped<IUserService, UserManager>();
but you are trying to resolve the implementation UserManager in the shown attribute
[ContainsFilter(typeof(UserManager), typeof(User), IdSections.FromRoute)]
call
[ContainsFilter(typeof(IUserManager), typeof(User), IdSections.FromRoute)]
instead
So I have created a provider which will handle all my code.
Originally it looked like this:
public class AnswerProvider : ApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly AnswerService _answerService;
private QuestionService _questionService;
public QuestionService QuestionService => _questionService ?? (_questionService = new QuestionService(this._unitOfWork));
public AnswerProvider(IUnitOfWork unitOfWork)
{
this._unitOfWork = unitOfWork;
this._answerService = new AnswerService(unitOfWork);
}
public async Task<IHttpActionResult> CreateAsync(AnswerRequestModel model)
{
try
{
// Validate our answer count
await ValidateAnswerCountAsync(model.QuestionId);
// Create our model
var answer = ModelFactory.Create(model);
// Add our images to our answer
answer.Images = model.Images;
// Save our model
this._answerService.Create(answer);
// Save the database changes
await this._unitOfWork.SaveChangesAsync();
// Return our updated model
return Ok(ModelFactory.Create(answer));
// If there is an error
}
catch (Exception ex)
{
// Return our error
return BadRequest(ex.Message.ToString());
}
}
/// <summary>
/// Validates the answers based on the question type
/// </summary>
/// <param name="id">The id of the question</param>
/// <returns></returns>
private async Task ValidateAnswerCountAsync(int id)
{
// Get our question
var question = await this.QuestionService.GetAsync(id, "Answers");
// If we have 3 answers or more
if (question.Answers.Count >= 3 && question.Type == QuestionType.Boolean)
{
// Throw an error
throw new InvalidOperationException("A Boolean question can only have 3 answers");
}
}
}
I inherited ApiController because I want to gain access to the Ok, BadRequest and other such methods, that is the only reason.
When I try to run that code, even though it compiles I get this error:
HttpControllerContext.Configuration must not be null
I assume that is because I am trying to inherit the ApiController and I shouldn't be doing that.
Is there another way I can get access the the Ok and other similar methods without inheriting the ApiController.
Please bare in mind that I will have more than one provider.
Do not inherit from ApiController as this is instantiated by a factory in the request pipeline. You should only inherit it for actual api controller instances, not for convenience of some of the existing methods. The best solution would be to throw custom exceptions in your Provider/Service/ whatever and catch them in your controller and return the correct HttpStatus OR let the exception pass through and it would result in a 500 status.
As requested though I have created a small wrapper around the ApiController that you could reuse in your Provider/Service/etc based on an interface (so its easy to abstract this AND easy to test).
// demo of controller calling your Provider
public class SomeController : ApiController
{
public async Task<IHttpActionResult> Get()
{
var wrapper = this.ActionWrapper();
var answerProvider = new AnswerProvider(wrapper);
var result = await answerProvider.CreateAsync(model);
}
}
// a simple extension on the ApiController
public static class WrapperExtension
{
public static IActionWrapper ActionWrapper(this ApiController controller)
{
return new ApiActionWrapperContext(controller);
}
}
// wrapped in interface so its easy to unit test the Provider
public interface IActionWrapper
{
OkResult Ok();
BadRequestResult BadRequest();
BadRequestErrorMessageResult BadRequest(string message);
OkNegotiatedContentResult<T> Ok<T>(T content);
}
// the implementation, this takes the current Controller and uses it as the context to return the same result types
// only implemented Ok and BadRequest as a demo, you can extend it as needed
public class ApiActionWrapperContext : IActionWrapper
{
private ApiController _controller;
public ApiActionWrapperContext(ApiController controller)
{
_controller = controller;
}
public BadRequestResult BadRequest()
{
return new BadRequestResult(_controller);
}
public BadRequestErrorMessageResult BadRequest(string message)
{
return new BadRequestErrorMessageResult(message, _controller);
}
public OkResult Ok()
{
return new OkResult(_controller);
}
public OkNegotiatedContentResult<T> Ok<T>(T content)
{
return new OkNegotiatedContentResult<T>(content, _controller);
}
}
// provider shortered with just some relevant code to demo
// notice constructor, the new private field, and the use of it
public class AnswerProvider
{
private IActionWrapper _actionWrapper;
public AnswerProvider(IActionWrapper actionWrapper)
{
if(actionWrapper == null)
throw new ArgumentNullException("actionWrapper");
_actionWrapper = actionWrapper;
}
public async Task<IHttpActionResult> CreateAsync(AnswerRequestModel model)
{
try
{
// Validate our answer count
await ValidateAnswerCountAsync(model.QuestionId);
// Create our model
var answer = ModelFactory.Create(model);
// Add our images to our answer
answer.Images = model.Images;
// Save our model
this._answerService.Create(answer);
// Save the database changes
await this._unitOfWork.SaveChangesAsync();
// Return our updated model
return this._actionWrapper.Ok(ModelFactory.Create(answer));
// If there is an error
}
catch (Exception ex)
{
// Return our error
return this._actionWrapper.BadRequest(ex.Message.ToString());
}
}
}
Right now my ViewModel looks like this:
public class MyViewModel
{
private readonly IMyService myService;
public ClaimantSearchViewModel(IMyService myService)
{
this.myService = myService;
}
}
My Controller that consumes this ViewModel looks like this:
public class MyController : Controller
{
private readonly IMyService myService;
public HomeController(IMyService myService)
{
this.myService = myService;
}
public IActionResult Index()
{
var model = new MyViewModel(myService);
return View(model);
}
[HttpPost]
public async Task<IActionResult> Find()
{
var model = new MyViewModel(myService);
await TryUpdateModelAsync(model);
return View("Index", model);
}
}
What I need is my Controller to look like is this:
public class MyController : Controller
{
private readonly IServiceProvider servicePovider;
public MyController(IServiceProvider servicePovider)
{
this.servicePovider = servicePovider;
}
public IActionResult Index()
{
var model = servicePovider.GetService(typeof(MyViewModel));
return View(model);
}
[HttpPost]
public IActionResult Index(MyViewModel model)
{
return View(model);
}
}
Right now, calling the first Index method works fine (with
builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource(x => x.Name.Contains("ViewModel")));
in my Startup class) but doing the POST to Index(MyViewModel model) gives you a No parameterless constructor defined for this object exception. I realize that a custom model binder that can use my DI will be the most likely solution... but I'm not able to find any help on how to even get started here. Please help me with this, especially for Autofac in MVC 6.
We got the answer here: https://github.com/aspnet/Mvc/issues/4167
And the answer is to use: [FromServices]
My Model ends up looking like this:
public class MyViewModel
{
[FromServices]
public IMyService myService { get; set; }
public ClaimantSearchViewModel(IMyService myService)
{
this.myService = myService;
}
}
Although it's sad to make that property public, it's much less sad than having to use a custom model binder.
Also, supposedly you should be able to pass [FromServices] as part of the param in the Action method, it does resolve the class, but that breaks the model binding... ie none of my properties got mapped. It looks like this: (but again, THIS DOES NOT WORK so use the above example)
public class MyController : Controller
{
... same as in OP
[HttpPost]
public IActionResult Index([FromServices]MyViewModel model)
{
return View(model);
}
}
UPDATE 1
After working with the [FromServices] attribute we decided that property injection in all of our ViewModels was not the way we wanted to go, especially when thinking about long term maintenance with testing. SO we decided to remove the [FromServices] attributes and got our custom model binder working:
public class IoCModelBinder : IModelBinder
{
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
var serviceProvider = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var model = serviceProvider.GetService(bindingContext.ModelType);
bindingContext.Model = model;
var binder = new GenericModelBinder();
return binder.BindModelAsync(bindingContext);
}
}
It's registered like this in the Startup ConfigureServices method:
services.AddMvc().AddMvcOptions(options =>
{
options.ModelBinders.Clear();
options.ModelBinders.Add(new IoCModelBinder());
});
And that's it. (Not even sure that options.ModelBinders.Clear(); is needed.)
UPDATE 2
After going through various iterations of getting this to work (with help https://github.com/aspnet/Mvc/issues/4196), here is the final result:
public class IoCModelBinder : IModelBinder
{
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{ // For reference: https://github.com/aspnet/Mvc/issues/4196
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
if (bindingContext.Model == null && // This binder only constructs viewmodels, avoid infinite recursion.
(
(bindingContext.ModelType.Namespace.StartsWith("OUR.SOLUTION.Web.ViewModels") && bindingContext.ModelType.IsClass)
||
(bindingContext.ModelType.IsInterface)
)
)
{
var serviceProvider = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var model = serviceProvider.GetRequiredService(bindingContext.ModelType);
// Call model binding recursively to set properties
bindingContext.Model = model;
var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(bindingContext);
bindingContext.ValidationState[model] = new ValidationStateEntry() { SuppressValidation = true };
return result;
}
return await ModelBindingResult.NoResultAsync;
}
}
You'd obviously want to replace OUR.SOLUTION... with whatever the namespace is for your ViewModels Our registration:
services.AddMvc().AddMvcOptions(options =>
{
options.ModelBinders.Insert(0, new IoCModelBinder());
});
UPDATE 3:
This is the latest iteration of the Model Binder and its Provider that works with ASP.NET Core 2.X:
public class IocModelBinder : ComplexTypeModelBinder
{
public IocModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, ILoggerFactory loggerFactory) : base(propertyBinders, loggerFactory)
{
}
protected override object CreateModel(ModelBindingContext bindingContext)
{
object model = bindingContext.HttpContext.RequestServices.GetService(bindingContext.ModelType) ?? base.CreateModel(bindingContext);
if (bindingContext.HttpContext.Request.Method == "GET")
bindingContext.ValidationState[model] = new ValidationStateEntry { SuppressValidation = true };
return model;
}
}
public class IocModelBinderProvider : IModelBinderProvider
{
private readonly ILoggerFactory loggerFactory;
public IocModelBinderProvider(ILoggerFactory loggerFactory)
{
this.loggerFactory = loggerFactory;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.Metadata.IsComplexType || context.Metadata.IsCollectionType) return null;
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
foreach (ModelMetadata property in context.Metadata.Properties)
{
propertyBinders.Add(property, context.CreateBinder(property));
}
return new IocModelBinder(propertyBinders, loggerFactory);
}
}
Then in Startup:
services.AddMvc(options =>
{
// add IoC model binder.
IModelBinderProvider complexBinder = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(ComplexTypeModelBinderProvider));
int complexBinderIndex = options.ModelBinderProviders.IndexOf(complexBinder);
options.ModelBinderProviders.RemoveAt(complexBinderIndex);
options.ModelBinderProviders.Insert(complexBinderIndex, new IocModelBinderProvider(loggerFactory));
This question is tagged with ASP.NET Core, so here's our solution for dotnet core 3.1.
Outline of our solution: TheProject needs to make ICustomerService available to an object created automatically in the request pipeline. Classes that need this are tagged with an interface, IUsesCustomerService. This interface is then checked by the Binder on object creation, and special case is handled.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
namespace TheProject.Infrastructure.DependencyInjection
{
/// <summary>
/// This is a simple pass through class to the binder class.
/// It gathers some information from the context and passes it along.
/// </summary>
public class TheProjectModelBinderProvider : IModelBinderProvider
{
public TheProjectModelBinderProvider()
{
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
ILoggerFactory ilogger;
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// The Binder that gets returned is a <ComplexTypeModelBinder>, but I'm
// not sure what side effects returning early here might cause.
if (!context.Metadata.IsComplexType || context.Metadata.IsCollectionType)
{
return null;
}
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
foreach (ModelMetadata property in context.Metadata.Properties)
{
propertyBinders.Add(property, context.CreateBinder(property));
}
ilogger = (ILoggerFactory)context.Services.GetService(typeof(ILoggerFactory));
return new TheProjectModelBinder(propertyBinders, ilogger);
}
}
/// <summary>
/// Custom model binder.
/// Allows interception of endpoint method to adjust object construction
/// (allows automatically setting properties on an object that ASP.NET creates for the endpoint).
/// Here this is used to make sure the <see cref="ICustomerService"/> is set correctly.
/// </summary>
public class TheProjectModelBinder : ComplexTypeModelBinder
{
public TheProjectModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, ILoggerFactory loggerFactory)
: base(propertyBinders, loggerFactory)
{
}
/// <summary>
/// Method to construct an object. This normally calls the default constructor.
/// This method does not set property values, setting those are handled elsewhere in the pipeline,
/// with the exception of any special properties handled here.
/// </summary>
/// <param name="bindingContext">Context.</param>
/// <returns>Newly created object.</returns>
protected override object CreateModel(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var customerService = (ICustomerService)bindingContext.HttpContext.RequestServices.GetService(typeof(ICustomerService));
bool setcustomerService = false;
object model;
if (typeof(IUsesCustomerService).IsAssignableFrom(bindingContext.ModelType))
{
setcustomerService = true;
}
// I think you can also just call Activator.CreateInstance here.
// The end result is an object that's constructed, but no properties are set yet.
model = base.CreateModel(bindingContext);
if (setcustomerService)
{
((IUsesCustomerService)model).SetcustomerService(customerService);
}
return model;
}
}
}
Then in the startup code, make sure to set AddMvcOptions.
public void ConfigureServices(IServiceCollection services)
{
// ...
// asp.net core 3.1 MVC setup
services.AddControllersWithViews()
.AddApplicationPart(assembly)
.AddRazorRuntimeCompilation()
.AddMvcOptions(options =>
{
IModelBinderProvider complexBinder = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(ComplexTypeModelBinderProvider));
int complexBinderIndex = options.ModelBinderProviders.IndexOf(complexBinder);
options.ModelBinderProviders.RemoveAt(complexBinderIndex);
options.ModelBinderProviders.Insert(complexBinderIndex, new Infrastructure.DependencyInjection.TheProjectModelBinderProvider());
});
}