Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Workaround for using type parameter in attribute for using ProducesResponseType with a generic type argument?

Tags:

c#

asp.net

I have a generic ASP.NET Core WebApi controller like:

public abstract class EntityController<TEntity> 
{
    public IActionResult Get(string id)
    {
        var entity = ... //load from database by id
        if (entity != null) 
            return new JsonResult(value, this.SerializerSettings()) {StatusCode  200};
        return NotFound();
    }
}

and I want to apply following attributes on the Get() method:

[ProducesResponseType(typeof(TEntity), 200)] //this causes compilation error.
[ProducesResponseType(typeof(Object), 404)]

For now, the only work around is to override each method in derived controller and add attributes there:

public class DerivedController :EntityController<MyEntity>
{
    [ProducesResponseType(typeof(TEntity), (int) HttpStatusCode.OK)]
    [ProducesResponseType(typeof(Object), (int) HttpStatusCode.NotFound)]
    public IActionResult Get(string id)
    {
        return base.Get(id);
    }
}

Ihis is very inconvenient that I should override every REST methods in every controller, just for use the concrete TEntity type in attributes. :-(

Any better work arounds?

like image 673
Smartkid Avatar asked Aug 25 '16 12:08

Smartkid


2 Answers

Since .NET Core 2.1 instead of using IActionResult, you can use ActionResult<TEntity> as returntype (or Task<ActionResult<TEntity>>) and then swagger will also know the returntype for 200 calls!

like image 148
Maximc Avatar answered Oct 26 '22 09:10

Maximc


Althrough I found no way to use generic type parameter in ProducesResponseTypeAttribute, I found another way to make swagger work:

Use IApplicationModelConvention to update ApplicationModel, which is used by swagger.

public class EntityControllerConversion : IApplicationModelConvention
{
    public void Apply(ApplicationModel application)
    {
        ActionModel action = ... // finds the controller action 
        Type viewModelType = ... // get the view type by reflection from the controller
        SetResponseUsingHack(action, viewModelType, HttpStatusCode.OK);
    }

    private void SetResponseUsingHack(ActionModel actionModel, Type responseType, HttpStatusCode statusCode)
    {
        if (actionModel == null) throw new ArgumentNullException(nameof(actionModel));
        if (responseType == null) throw new ArgumentNullException(nameof(responseType));

        var writable = (IList<object>)(actionModel.Attributes);
        var attribute = FindResponseAttributeUsingHack(writable, statusCode);
        if (attribute != null)
        {
            attribute.Type = responseType;
        }
    }

    private ProducesResponseTypeAttribute FindResponseAttributeUsingHack(IList<object> attributes, HttpStatusCode statusCode)
    {
        if (attributes == null) return null;
        var result = attributes.OfType<ProducesResponseTypeAttribute>()
            .Where(x => x.Type == typeof(ProducesResponseStub))
            .FirstOrDefault(x => x.StatusCode == (int) statusCode);
        return result;
    }
}

public abstract class EntityController<TEntity> 
{
    [HttpGet]
    [ProducesResponseType(typeof(ProducesResponseStub), 200)]
    public IActionResult Get(string id)
    {
    }
}

public static class ProducesResponseStub
{
}

NOTE: Swagger won't work correctly if you just add a new ProducesResponseTypeAttribute instance to ActionModel.Attributes, may be it's a bug in swagger or in asp.net core. That why I use ProducesResponseStub in decorating action methods in EntityController and replace them with correct types in EntityControllerConversion.

like image 33
Smartkid Avatar answered Oct 26 '22 08:10

Smartkid