Returning a generic object without knowing the type?
Asked Answered
G

1

6

I'm still fairly new to programming and have been tasked with creating a WebHook consumer that takes in a raw JSON string, parses the JSON into an object, which will be passed into a handler for processing. The JSON is coming in like this:

{
   "id":"1",
   "created_at":"2017-09-19T20:41:23.093Z",
   "type":"person.created",
   "object":{
      "id":"person1",
      "created_at":"2017-09-19T20:41:23.076Z",
      "updated_at":"2017-09-19T20:41:23.076Z",
      "firstname":"First",
      ...
   }
}

The inner object can be any object so I thought this would be a great opportunity to use generics and built my class as follows:

public class WebHookModel<T> where T : class, new()
{
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "created_at")]
    public DateTime CreatedAt { get; set; }

    [JsonProperty(PropertyName = "type")]
    public string Type { get; set; }

    [JsonProperty(PropertyName = "object")]
    public T Object { get; set; }

    [JsonIgnore]
    public string WebHookAction
    {
        get
        {
            return string.IsNullOrEmpty(Type) ? string.Empty : Type.Split('.').Last();
        }
    }
}

Then created the following interface:

public interface IWebHookModelFactory<T> where T : class, new()
{
   WebHookModel<T> GetWebHookModel(string type, string jsonPayload);
}

What I'm failing to understand is how am I supposed to implement the Factory class without knowing what the type is at compile time?

Playing around with the Model a bit, I changed it to an abstract class with an abstract T object so that it could be defined by a derived class.

public abstract class WebHookModel<T> where T : class, new()
{
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "created_at")]
    public DateTime CreatedAt { get; set; }

    [JsonProperty(PropertyName = "type")]
    public string Type { get; set; }

    [JsonProperty(PropertyName = "object")]
    public abstract T Object { get; set; }

    [JsonIgnore]
    public string WebHookAction
    {
        get
        {
            return string.IsNullOrEmpty(Type) ? string.Empty : Type.Split('.').Last();
        }
    }
}

public PersonWebHookModel : WebHookModel<Person>
{
    public override Person Object { get; set; }
}

But I still run into the same issue of trying to implement an interface in which I don't know the type at runtime. From what I've found online, this is an example of covariance, but I haven't found any articles that explain how to resolve this issue. Is it best to skip generics and create a massive case statement?

public interface IWebHookFactory<TModel, TJsonObject> 
    where TJsonObject : class, new()
    where TModel : WebHookModel<TJsonObject>
{
    TModel GetWebHookModel(string type, string jsonPayload);
}

I'm a bit partial to using the abstract class approach because it lets me define individual handlers based on which model I'm passing into my Service.

public interface IWebHookService<TModel, TJsonObject>
    where TJsonObject : class, new()
    where TModel : WebHookModel<TJsonObject>
{
    void CompleteAction(TModel webHookModel);
}

public abstract class BaseWebhookService<TModel, TJsonObject> : IWebHookService<TModel, TJsonObject>
        where TJsonObject : class, new()
        where TModel : WebHookModel<TJsonObject>
{
    public void CompleteAction(TModel webHookModel)
    {
        var self = this.GetType();
        var bitWise = System.Reflection.BindingFlags.IgnoreCase
                        | System.Reflection.BindingFlags.Instance
                        | System.Reflection.BindingFlags.NonPublic;

        var methodToCall = self.GetMethod(jsonObject.WebHookAction, bitWise);
        methodToCall.Invoke(this, new[] { jsonObject });
    }

    protected abstract void Created(TModel webHookObject);

    protected abstract void Updated(TModel webHookObject);

    protected abstract void Destroyed(TModel webHookObject);
}

public class PersonWebHookService : BaseWebHookService<PersonWebHookModel, Person>
{
    protected override void Created(PersonWebHookModel webHookModel)
    {
        throw new NotImplementedException();
    }

    protected override void Updated(PersonWebHookModel webHookModel)
    {
        throw new NotImplementedException();
    }

    protected override void Destroyed(PersonWebHookModel webHookModel)
    {
        throw new NotImplementedException();
    }
}
Gorgias answered 20/9, 2017 at 16:50 Comment(3)
If the only information you have of T is runtime type information (Type, name or what have you) then don't use generics, it's not the right tool for the job; you'd end up with WebHookModel<object> which is pointless; simply use the non generic version of the interface.Karelian
What exactly is being done with these objects? How will they be handled downstream? Is there one handler for all of them or do you have a handler for each type?Unthinking
For each WebHook object coming in, I've found 10 so far, it will need to be handled differently. BaseWebHookService and PersonWebHookService are examples of how each object will be handled. The convention from their API documentation is object.action (action being Created, Updated, or Destroyed). The BaseWebHookService knows which derived service to use based on which WebHookModel is being passed in.Gorgias
M
2

Key points for the solution: 1. There needs to be some virtual call in there somewhere. 2. Somehow you need to map from your type tag in your JSON payload to your actual C# class.
IE, "person.created"," --> 'Person'.
If you control the serialization format, JSON.Net can inject its own type tag and do this for you. Assuming you can't go that route ... So you'll need something like a Dictionary to contain the mapping.

Assuming your definitions is like:

abstract class WebhookPayload // Note this base class is not generic! 
{
    // Common base properties here 

    public abstract void DoWork();
}

abstract class PersonPayload : WebhookPayload
{
    public override void DoWork()
    {
        // your derived impl here 
    }
}

And then you can deserialize like:

    static Dictionary<string, Type> _map = new Dictionary<string, Type>
    {
        { "person.created", typeof(PersonPayload)}
    }; // Add more entries here 

    public static WebhookPayload Deserialize(string json)
    {
        // 1. only parse once!
        var jobj = JObject.Parse(json); 

        // 2. get the c# type 
        var strType = jobj["type"].ToString(); 

        Type type;
        if (!_map.TryGetValue(strType, out type))
        {
            // Error! Unrecognized type
        }

        // 3. Now deserialize 
        var obj = (WebhookPayload) jobj.ToObject(type);
        return obj;
    }
Maladjustment answered 20/9, 2017 at 20:0 Comment(2)
So there wouldn't be any way of creating a mapper dictionary like Dictionary<string, Func<string, WebHookModel<TJson>> factoryMapper because the factory would have to have a concrete implementation? I guess I don't understand why I couldn't have the factory class implement my IWebHookFactory<TModel, TJsonObject> since the method will be returning a concrete class (i.e. PersonWebHookModel). Perhaps I need to remove the generic and use WebHookModel as a base class.Gorgias
Right. You can't have Dictionary<string, Func<string, WebHookModel<TJson>> factoryMapper because it has an unresolved generic TJson in there. So the trick here is to a) have a non-generic base class that has virtuals; b) have anything generics on the derived class; which can implement the virtual and leverage its generic properties in that implementation.Maladjustment

© 2022 - 2024 — McMap. All rights reserved.