Brian Kessler
Brian Kessler

Reputation: 2327

Yet another question how to reduce code duplication in C#

I have two objects, lets call them A and B.

Each contain the following property:

        [IgnoreDataMember]
        public string SalesforceId { get; set; }

Then I have another two objects, lets call them UpdatedA and UpdatedB, which respectively extend A and B, and include nothing but:

        [DataMember(Name = "sf__Id")]
        public new string SalesforceId { get; set; }
        [DataMember(Name = "sf__Created")]
        public bool SalesforceCreated { get; set; }

The reason for this is so that I can use ServiceStack to convert A and B to CSV files and then use it again to convert CSV files from Salesforce back to C# Objects (If I don't ignore SalesforceId, the upload to Salesforce Bulk API 2.0 will fail).

So, the first question part of this question is do I really need to create two separate classes for UpdatedA and UpdatedB, as these classes are nearly identical and are actually both poltergeists, because I only use them in the following two methods:

        private Dictionary<string, A> Update(Dictionary<string, A> aByExternalIds, RelayerContext context) {
            IConfiguration config = context.Config;
            string url = $"{config["SalesforceInstanceBaseUrl"]}/services/data/{config["SalesforceVersion"]}/jobs/ingest/{context.job.Id}/successfulResults";
            
            this.restClient.Get(url, context.token)
                .FromCsv<List<UploadedA>>()
                .ForEach((updatedA) => {
                    if (aByExternalIds.TryGetValue(updatedA.ExternalId, out A oldA)) {
                        oldA.SalesforceId = updatedA.SalesforceId;
                    }
                });

            return aByExternalIds;
        }

        private Dictionary<string, B> Update(Dictionary<string, B> bBySalesforceAId, RelayerContext context) {
            IConfiguration config = context.Config;
            string url = $"{config["SalesforceInstanceBaseUrl"]}/services/data/{config["SalesforceVersion"]}/jobs/ingest/{context.job.Id}/successfulResults";

            this.restClient.Get(url, context.token)
                .FromCsv<List<UploadedB>>()
                .ForEach((updatedB) => {
                    if (bBySalesforceAId.TryGetValue(updatedB.A__c, out B oldB)) {
                        oldB.SalesforceId = updatedB.SalesforceId;
                    }
                });

            return bBySalesforceAId;
        }

Which leads to the second part of this question.

Both of these questions are very similar. We can see that the inputs are mapped by different properties on A and B... so I think I could do something like create an interface:

    public interface Identifiable {
        public string getIdentifier();
    }

which would could be used to return either updatedA.ExternalId or updatedB.A__c.

But I'm not sure what the method signature would look like if I'm using generics. Also, if I don't know how I could handle FromCsv<List<UploadedA>>() and FromCsv<List<UploadedB>>() in a generic way (maybe passing in a function?)

Anyway, to sum up, what I'd like to do is reduce those these two methods to just one, and if I can remove one or both of those Uploaded classes, so much the better.

Any ideas?

Upvotes: 2

Views: 53

Answers (1)

JonasH
JonasH

Reputation: 36629

How about something like this:

public interface IBase
{
    string SalesforceId { get; set; }
}
public class A : IBase
{
    public string SalesforceId { get; set; }
}
public class UploadedA : A
{
    public new string SalesforceId { 
        get => base.SalesforceId;
        set => base.SalesforceId = value; }
    public bool SalesforceCreated { get; set; }
}

public static void Update<T, TU>(Dictionary<string, T> oldBySalesForceId, Func<TU, string> updatedId)
        where TU : T
        where T : IBase
{
    // Call service and read csv to produce a list of uploaded objects...
    // Substituting with an empty list in the example
    var list = new List<TU>();
    foreach (var updated in list)
    {
        if (oldBySalesForceId.TryGetValue(updatedId(updated), out var old))
        {
            old.SalesforceId = updated.SalesforceId;
        }
    }
}

I have removed some details that did not seem relevant for the example. This uses generics with constraints and a interface to ensure both the updated and old value has a SalesForceId.

I changed the derived class so that it uses the same SalesforceId as the base class, you could change it to virtual/override if you prefer, but it is probably not a good idea that the base and derived class both have independent properties with the same name since it will be confusing.

It uses a delegate to describe the id/key for UpdatedA/UpdatedB. You could use an interface instead if you prefer.

Upvotes: 1

Related Questions