Generic type controller methods in ASP.NET Core
Image from Pexels by cottonbro

Generic type controller methods in ASP.NET Core

Using generic type controller methods in ASP.NET Core with Swagger

Generic types are great way to re-use methods and apply the same logic to different types. However, they come with some limitations, especially in ASP.NET realm. For example, you cannot use generic types for controllers or controller methods.

I had a specific case while working on my Webhook Server implementation with ASP.NET Core. What I wanted to achieve is to have one single endpoint for which I can pass instances of objects of different types. Basically this endpoint does the same thing for each type and that is to publish it to the message bus, so it would not make much sense to have duplicated exact same method body to handle different type, just because this is not in ASP.NET Core out of the box. 

What I also wanted to accomplish is to have proper Swagger UI for these controller methods with proper sample of the model. To simplify the problem and a solution I created a simple ASP.NET Core API project which tries to tackle this issue in a bit different way by relying heavily on Open API documentation and Swagger UI.

Narrowing down generic types

Since the solution in this article relies heavily on reflection to detect the types which will be used as generic types for the web methods in ASP.NET Core Web API controllers, we need to narrow down those types. These will be our models which we will POST in out request, kinda POST<T>.

The easiest way to do this is to use simple marker interface which is basically just an empty interface definition. Now in your case you may want to have a common property for all the models which implement this interface which is also one of the benefits of using an interface for all the models. I our case we'll just use and empty, marker interface to mark out models.

namespace Generics.Sample.Api.Models{public interface IEventModel
    {
    }
}
    

We have our marker interface and we can now create models which will be implementing this interface. For the demo purpose, I create two sample models

namespace Generics.Sample.Api.Models
{
    public class SampleEvent1Model:IEventModel
    {
        public Guid Id { get; set; }
        public String Description { get; set; }
        public DateTime DateTime { get; set; }

        public SampleEvent1Model()
        {
            Id = Guid.NewGuid();
            Description = "NO DESCRIPTION";
            DateTime = DateTime.UtcNow;
        }
    }

    public class SampleEvent2Model:IEventModel
    {
        public int Order { get; set; }
        public bool IsAutogenerated { get; set; }
        public String Info { get; set; }
    }
}

    

At this point we achieved to know what our generic types can be and we'll see the benefits of doing this in the following text.

Creating generic controller method

As I mentioned at the beginning, the most common reason of using generic methods is to reduce the code which handles certain structure od the data. In ASp.NET Core, most common reason for the generic controller method is underlying service or handler class which will take over procession of the payload from ASP.NET Core facade by receiving strongly types structured data from the facade layer.

It is highly likely that your controller method will have this service passed to it's constructor via DI.

namespace Generics.Sample.Api.Services
{
    public interface ISampleService
    {
        Task ProcessEventModel<T>(T model) where T : class, IEventModel;
    }

    public class SampleService : ISampleService
    {
        readonly ILogger<SampleService> _logger;
        public SampleService(ILogger<SampleService> logger)
        {
            _logger = logger;
        }

        public async Task ProcessEventModel<T>(T model) where T:class, IEventModel
        {
            _logger.LogInformation($"Processed model of type {typeof(T).FullName}");
            await Task.CompletedTask;
        }
    }
}
    

Since we will have ISampleService instance added to DI contained with scoped lifetime, we need to reference the instance of the service inside the controller by accepting it in the constructor as we would do with any other service added to DI container in the Startup.cs inside ConfigureServices method.

namespace Generics.Sample.Api.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class EventsController : ControllerBase
    {
        readonly ISampleService _sampleService;

        public EventsController(ISampleService sampleService)
        {
            _sampleService = sampleService;
        }

        [HttpPost("{type}")]
        public async Task<ActionResult> PublishEvent(String type, Object model)
        {
            // TODO: Invoke _sampleService.ProcessEventModel<T>(T model)

            throw new NotImplementedException();
        }
    }
}
    

The generic method does not actually reference generic type in a standard fashion way like we would normally do and how it is done in our ISampleService.ProcessEventModel<T> method. Instead it splits the generic type T into two components and those are full type name and actual instance of the class. 

Swagger Object 

Now this all may look pretty loose, but we'll be using Swagger to force the client to pass the right payload. You can oviusly see from here that we cannot directly call our service method, but thanks to reflection, we can dynamically create a delegate to this method using reflection.

Before we do that, we need to retrieve the available model types which will be validated and used to create paths in our service Open API definition.

I in general avoid to use so called "Helper" classes because they tend to end up too cluttered and developers often put unrelated stuff al together inside them. Insted I prefer extension method, so for this case I will also use extension methods with few overloads

namespace Generics.Sample.Api.Extensions
{
    public static class EventTypesExtensions
    {
        public static IEnumerable<Type> GetEventTypes(this Assembly assembly)
        {
            IEnumerable<Type> _eventTypes = typeof(IEventModel).Assembly.GetTypes()
                .Where(t => !t.IsAbstract && t.IsPublic && typeof(IEventModel).IsAssignableFrom(t))
                .OrderBy(t => t.Name);

            return _eventTypes;
        }

        public static IEnumerable<Type> GetEventTypesFromParentAssembly(this Type type)
        {
            return type.Assembly.GetEventTypes();
        }

        public static IEnumerable<Type> GetEventTypesFromParentAssembly(this Object @object)
        {
            return @object.GetType().GetEventTypesFromParentAssembly();
        }
    }
}
    

Dynamically invoking generic method 

As you could see, we cannot just directly call ISampleSerice.ProcessEventModel<T> method. Instead we'll use reflection to first find this method in the ISampleService instance and than simple invoke it with model object, but before we do that we need to validate and cast our payload.

Model payload comes as an instance of Object type. Anything can be an Object so we do not know much about the instance driven by the payload, but we have our type name argument which we can use to find the type and cast the payload object.

namespace Generics.Sample.Api.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class EventsController : ControllerBase
    {
        readonly ISampleService _sampleService;

        public EventsController(ISampleService sampleService)
        {
            _sampleService = sampleService;
        }

        [HttpPost("{type}")]
        public async Task<ActionResult> PublishEvent(String type, Object model)
        {
            var eventType = this.GetType().GetEventTypesFromParentAssembly().SingleOrDefault(t => t.FullName == type);
            if (eventType == null)
                throw new ArgumentException($"{type} is not valid event type");
            var eventModel = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(model), eventType);

            var task =  (Task)_sampleService.GetType()
                            .GetMethod(nameof(ISampleService.ProcessEventModel))
                            .MakeGenericMethod(eventType).Invoke(_sampleService, new[] { eventModel });

            await task;
            return Accepted();
        }
    }
}
    

Unfortunately, direct cast in this case would not work so I used a dirty hack of simple serializing and deserializing the model instance using Newtnsoft.Json library and methods JsonConvert.SerailizeObject/JsonConvert.DesirializeObject.

This way we managed to cast our payload to a proper type and pass it to the service instance which will be processing it.

Things are still pretty loose and without someone knowing actual types and its structure it would be quite difficult to invoke our endpoint. Of course we could expose the list of type names, but we are still missing the payload structure and therefore we would have to comply to poor design and a lot of manual work to ensure we are sending the proper values.

Using something like NSwag or Autorest to generate clients would be useless and won't make it any easier to consume the service. Luckily Swagger is pretty flexible and we can easily manipulate with Open API definition from our code which is what we are about to do to make our service more usable by the clients.

Generating endpoints dynamically for each supported type

Alright, we got it all working, but from the usability stand point we far behind of having easy to use service by our clients. What we are bout to do is to basically nicely wrap our generic controller method by describing it in details in Open API documentation via Swagger.

So here is the list of steps we are going to perform to achieve higher usability of our service:

  • make type name route parameter available in the route automatically
  • provide the proper model sample in Swagger UI
  • hide our current generic method so that clients do not get confused
Note

Another useful thing you definitely are going to need for your generic type models is validation. Unfortunately I will not be covering this subject in this article, but if you want to get on with some simple attribute based validation than I suggest you have a look at Webhook Server repository on github https://bit.ly/3ss2c5o

To achieve this we need to dig into the Open API generation process from our code. Swagger is pretty flexible for doing something like this and for this I wrote the following IDocumentFilter implementation


namespace Webhooks.Api.Swagger
{
    public class EventPublishDocumentFilter : IDocumentFilter
    {
        readonly IEnumerable<Type> _events;
        public EventPublishDocumentFilter()
        {
            _events = this.GetType().GetEventTypesFromParentAssembly();
        }
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {

            OpenApiMediaType getOpenApiMediaType(Type type)
            {
                // Get media type with example for the type
                return new OpenApiMediaType()
                {
                    Example = new OpenApiString(JsonConvert.SerializeObject(Activator.CreateInstance(type), Formatting.Indented)),
                    Schema = new OpenApiSchema
                    {
                        Type = "String"
                    }
                };
            }

            foreach (var @event in _events)
            {
                // Define sample payload
                swaggerDoc.Components.RequestBodies.Add(@event.FullName, new OpenApiRequestBody
                {
                    Content = new Dictionary<string, OpenApiMediaType>()
                        {
                            { "application/json-patch+json", getOpenApiMediaType(@event)},
                            { "application/json", getOpenApiMediaType(@event)},
                            { "text/json", getOpenApiMediaType(@event)},
                            { "application/*+json", getOpenApiMediaType(@event)}
                        }
                });
            }

            // Find route of the method
            var apiDescriptions = context.ApiDescriptions.Where(d => d.HttpMethod.Equals("POST", StringComparison.InvariantCultureIgnoreCase) &&
                                                                     d.RelativePath.Equals("events/{type}", StringComparison.InvariantCultureIgnoreCase) &&
                                                                     d.ParameterDescriptions.Any(p => p.Name.Equals("model", StringComparison.InvariantCultureIgnoreCase) &&
                                                                                                      p.Source.Id.Equals("Body", StringComparison.InvariantCultureIgnoreCase))
                                                                     );

            foreach (var apiDescription in apiDescriptions)
            {
                // Take into consideration version route segment if present
                var groupSegment = !String.IsNullOrWhiteSpace(apiDescription.GroupName) ? $"/{apiDescription.GroupName}" : String.Empty;
                foreach (var @event in _events)
                {
                    // Add new route
                    swaggerDoc.Paths.Add($"{groupSegment}/events/{@event.FullName}", new OpenApiPathItem()
                    {
                        Operations = new Dictionary<OperationType, OpenApiOperation>()
                        {
                            {
                                OperationType.Post, new OpenApiOperation()
                                {
                                    Tags = new List<OpenApiTag>(){
                                        new OpenApiTag() {Name="Events publishing" }
                                    },
                                    RequestBody = new OpenApiRequestBody()
                                    {
                                        Reference =  new OpenApiReference()
                                        {
                                            Type = ReferenceType.RequestBody,
                                            Id = $"components/requestBodies/{@event.FullName}",
                                            ExternalResource = ""
                                        }
                                    },
                                    Summary = $"Publishes {@event.FullName} event"
                                }
                            }
                        }
                    });
                }

                // Remove old route
                swaggerDoc.Paths.Remove($"{groupSegment}/{apiDescription.RelativePath}");
            }
        }

    }
}


    

This IDocumentFilter implemented class does all the things listed in the list above. Of course, to be able to initiate this code which will shape the final Open API documentation, you need to invoke it from the DI setup in Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRouting(options => options.LowercaseUrls = true);
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "Generics.Sample.Api", Version = "v1" });
                c.DocumentFilter<EventPublishDocumentFilter>();
            });

            services.AddScoped<ISampleService, SampleService>();
        }
    

We have everything in place so let's see how does the Swagger UI looks like now after engaging this IDocumentFilter interface implementation

Swagger Generic Collapsed

We have a separate url/path for each type that implement marker interface IDomainEvent, but this is not all we achieved with the IDocumentFilter interface implementation. You can see that one part of the code in this class (EventPublishDocumentFilter) actually takes care of the example part for each parameter. This is the place where you want to explain to the client via Open API how the model parameters should look like.

Since we used the generic method initially, we accepted Object instance, so we could not really create an example in Swagger UI. Unlike the generic method which accepts model type name as string in a route parameter and model (payload) as an Object we want to tell the client what is the structure of this object.

Now, we can easily obtain a type from a route name, but for the payload model we can use this type to create a sample instance and then show it serialized in Swagger UI. This is pretty much what I did as well int his example, so when you now expand the controller method you will see the sample structure of the model.

Swagger Generic Expanded1 

Swagger Generic Expanded2

With a simple default value setting in a model constructor, you can even pre-fill the model property values or just keep them as they are like in example above.

Conclusion

This approach may not look like many examples you are going to find on the internet because unlikely only to rely on ASP.NET Core Web API, it hevily relies on Swagger do show the structure of the call intended while keeping the flexibility of the generic types in the ASP.NET Core layer of the application.

Another, maybe the most import important benefit of this approach is that you can easily expand on more domain event. Just adding the model class which implements and empty, marker interface, your UI will dynamically update to accommodate these changes and expose a separate endpoint path for the now domain event model introduced.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article