4

I have an API with multiple endpoints. I'd like to add a property to all endpoint responses, without adding it to each endpoint response model individually.

Ex:

public class MyClass
{
    public string MyProperty { get; set; } = "Hello";
}

public class MyOtherClass
{
    public string MyOtherProperty { get; set; } = "World";
}

public class MyController : ControllerBase
{
    [HttpPost]
    public async Task<ActionResult<MyClass>> EndpointOne(POSTData data)
    {
        // implementation omitted
    }

    [HttpPost]
    public async Task<ActionResult<MyOtherClass>> EndpointTwo(POSTOtherData otherData)
    {
        // implementation omitted
    }
}

Calling either endpoint returns a JSON representation of MyClass or MyOtherClass as appropriate - i.e.

{ "MyProperty":"Hello" } or { "MyOtherProperty":"World" }

I want to add a property, say a string ApiName, to all endpoints in the API, so that the result of the above code would be either (as appropriate)

{ "MyProperty":"Hello", "ApiName":"My awesome API" } 

or

{ "MyOtherProperty":"World", "ApiName":"My awesome API" }

Is there a way to hook into the JSON-stringified result just before returning and add a top-level property like that? If so, I presume I'd have to wire it up in startup.cs, so I've been looking at app.UseEndpoints(...) methods, but haven't found anything that's worked so far. Either it's not added the property, or it's replaced the original result with the new property.

Thanks in advance!

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Brian
  • 137
  • 1
  • 6
  • 2
    Sounds like you need a middleware. – funatparties Apr 05 '22 at 20:44
  • 1
    Yes, you need a middleware. Heres a similar question with an answer https://stackoverflow.com/questions/37395227/add-response-headers-to-asp-net-core-middleware – Rani Sharim Apr 08 '22 at 10:40
  • 1
    See [Configuring and extending the WCF runtime with behaviors](https://learn.microsoft.com/en-us/dotnet/framework/wcf/extending/configuring-and-extending-the-runtime-with-behaviors#:~:text=There%20are%20four%20kinds%20of,and%20their%20associated%20EndpointDispatcher%20objects.) – John Wu Apr 09 '22 at 05:24

4 Answers4

7

Use Newtonsoft.Json in your net web api

Register a custom contract resolver in Startup.cs:

builder.Services.AddControllers()
    .AddNewtonsoftJson(options => options.SerializerSettings.ContractResolver = CustomContractResolver.Instance);

The implementation:

public class CustomContractResolver : DefaultContractResolver {
    public static CustomContractResolver Instance { get; } = new CustomContractResolver();

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = base.CreateProperties(type, memberSerialization);
        // add new property
        ...
        properties.Add(newProp);
        return properties;
    }}

See more Json.net Add property to every class containing of a certain type

Danut Radoaica
  • 1,860
  • 13
  • 17
5

You can add a base class with the shared property. Should work for both XML and JSON.

public class MyApiClass
{
    public string ApiName => "MyAwesomeApi";
}

public class MyClass : MyApiClass
{
    public string MyProperty { get; set; } = "Hello";
}

public class MyOtherClass : MyApiClass
{
    public string MyOtherProperty { get; set; } = "World";
}

public class MyController : ControllerBase
{
    [HttpPost]
    public async Task<ActionResult<MyClass>> EndpointOne(POSTData data)
    {
        // implementation omitted
    }

    [HttpPost]
    public async Task<ActionResult<MyOtherClass>> EndpointTwo(POSTOtherData otherData)
    {
        // implementation omitted
    }
}
user700390
  • 2,287
  • 1
  • 19
  • 26
0

My 0.02 cents says to implement an abstract base class.

Abstract class inheritance look similar to a standard inheritance.

    public class MyClass:MyAbstractClass
    {
        [JsonPropertyName("Class Property")]
        public string MyProperty { get; set; } = "Hello";
        }
    public class MyOtherClass:MyAbstractClass
    {
        [JsonPropertyName("Class Property")]
        public string MyOtherProperty { get; set; } = "World";
        }

However the abstract class will allow you to implement additional features in the event you need them in the future.

    public abstract class MyAbstractClass{

        [JsonPropertyName("API Name")]
        public string ApiName{get;set;}="My Aweomse API";

        //Just a thought if you want to keep track of the end point names
        //while keeping your object names the same
        [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
        public string EndPointName{
            get{
                return get_endpoint_name();
                }}
        private string get_endpoint_name(){
            return this.GetType().Name;
            }

        //May as well make it easy to grab the JSON
        [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
        public string As_JSON{
            get {
                return to_json();
                }}

        private string to_json(){
            
            object _myObject = this;
            string _out;

            JsonSerializerOptions options = 
                new JsonSerializerOptions { 
                        WriteIndented = true };
            
            _out = 
                JsonSerializer.Serialize(_myObject, options);

            return _out;
        }
    }

Probably should have implemented a generic return object, then you could just loop through the task results. I suppose you still can if you have the task return only the JSON string.

    public static void run(){

        Task<MyClass> _t0 = task0();
        Task<MyOtherClass> _t1 = task1();
        Task[] _tasks = new Task[]{_t0,_t1};

        Task.WhenAll(_tasks).Wait();
        
        Console.WriteLine(""
        +$"{_t1.Result.ApiName}:\n"
        +$"End Point: {_t1.Result.EndPointName}:\n"
        +$"JSON:\n{_t1.Result.As_JSON}");
        
        Console.WriteLine(""
        +$"{_t0.Result.ApiName}:\n"
        +$"End Point: {_t0.Result.EndPointName}:\n"
        +$"JSON:\n{_t0.Result.As_JSON}");
        
    }

    private static Task<MyClass> task0(){
        return Task.Run(()=>{
            Console.WriteLine("Task 0 Doing Something");
            return new MyClass();
        });
    }
    private static Task<MyOtherClass> task1(){
        return Task.Run(()=>{
            Console.WriteLine("Task 1 Doing Something");
            return new MyOtherClass();
        });
    }

And of course the aweosome...awesome:-) results:

Results

Another thought is that you could implement your two different tasks as abstract methods, but that's a different conversation all together.

Alex8695
  • 479
  • 2
  • 5
0

In addition to all of the great answers, I prefer to use Action Filter and ExpandoObject. In Program File you should add your custom action Filter.

builder.Services.AddControllers(opt =>
{
    opt.Filters.Add<ResponseHandler>();
});

and ResponseHandler acts like below:

public class ResponseHandler : IActionFilter
    {
        public void OnActionExecuted(ActionExecutedContext context)
        {
            IDictionary<string, object> expando = new ExpandoObject();

            foreach (var propertyInfo in (context.Result as ObjectResult).Value.GetType().GetProperties())
            {
                var currentValue = propertyInfo.GetValue((context.Result as ObjectResult).Value);
                expando.Add(propertyInfo.Name, currentValue);
            }
            dynamic result = expando as ExpandoObject;


            result.ApiName = context.ActionDescriptor.RouteValues["action"].ToString();
            context.Result = new ObjectResult(result);
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {

        }
    }