diff --git a/HubSpot.NET.Examples/Companies.cs b/HubSpot.NET.Examples/Companies.cs index 0b0a7f26..e0181d21 100644 --- a/HubSpot.NET.Examples/Companies.cs +++ b/HubSpot.NET.Examples/Companies.cs @@ -27,31 +27,44 @@ private static void Tests(HubSpotApi api) /** * Create a company */ - var company = api.Company.Create(new CompanyHubSpotModel() - { - Domain = "squaredup.com", - Name = "Squared Up" - }); + //var company = api.Company.Create(new CompanyHubSpotModel() + //{ + // Domain = "squaredup.com", + // Name = "Squared Up" + //}); /** * Update a company's property */ - company.Description = "Data Visualization for Enterprise IT"; - api.Company.Update(company); + //company.Description = "Data Visualization for Enterprise IT"; + //api.Company.Update(company); /** * Get all companies with domain name "squaredup.com" */ - var companies = api.Company.GetByDomain("squaredup.com", new CompanySearchByDomain() + //var companies = api.Company.GetByDomain("squaredup.com", new CompanySearchByDomain() + //{ + // Limit = 10 + //}); + + var companiesList = api.Company.List(new ListRequestOptions() { - Limit = 10 + Limit = 10, + //PropertiesToInclude = new List() { + // "name", + // "description", + // "companyId" + //} + //Offset = companyLists.Offset }); + Console.WriteLine($"Pass..."); + /** * Delete a contact */ - api.Company.Delete(company.Id.Value); + //api.Company.Delete(company.Id.Value); } } diff --git a/HubSpot.NET.Examples/Program.cs b/HubSpot.NET.Examples/Program.cs index c84fb9d2..9d06eb18 100644 --- a/HubSpot.NET.Examples/Program.cs +++ b/HubSpot.NET.Examples/Program.cs @@ -101,9 +101,9 @@ private static void RunApiKeyExamples(HubSpotApi hapiApi) //EmailSubscriptions.Example(hapiApi); //Deals.Example(hapiApi); //Contacts.Example(hapiApi); - //Companies.Example(hapiApi); + Companies.Example(hapiApi); //CompanyProperties.Example(hapiApi); - Pipelines.Example(hapiApi); + //Pipelines.Example(hapiApi); } private static void RunOAuthExamples(HubSpotApi oauthApi) diff --git a/HubSpot.NET/Api/Company/HubSpotCompanyApi.cs b/HubSpot.NET/Api/Company/HubSpotCompanyApi.cs index 78139306..537ae50d 100644 --- a/HubSpot.NET/Api/Company/HubSpotCompanyApi.cs +++ b/HubSpot.NET/Api/Company/HubSpotCompanyApi.cs @@ -1,5 +1,6 @@ namespace HubSpot.NET.Api.Company { + using Flurl; using HubSpot.NET.Api.Company.Dto; using HubSpot.NET.Core; using HubSpot.NET.Core.Abstracts; @@ -76,19 +77,43 @@ public CompanySearchResultModel GetByDomain(string domain, public CompanyListHubSpotModel List(ListRequestOptions opts = null) { + opts = opts ?? new ListRequestOptions(); - string path = GetRoute("companies", "paged"); + //string path = GetRoute("companies", "paged"); + var path = GetRoute("companies", "paged").SetQueryParam("limit", opts.Limit); + + //path += $"{QueryParams.COUNT}={opts.Limit}"; - path += $"{QueryParams.COUNT}={opts.Limit}"; + //if (opts.PropertiesToInclude.Any()) + // path += $"{QueryParams.PROPERTIES}={opts.PropertiesToInclude}"; if (opts.PropertiesToInclude.Any()) - path += $"{QueryParams.PROPERTIES}={opts.PropertiesToInclude}"; + path.SetQueryParam("properties", opts.PropertiesToInclude); + + //if (opts.Offset.HasValue) + // path += $"{QueryParams.OFFSET}={opts.Offset}"; if (opts.Offset.HasValue) - path += $"{QueryParams.OFFSET}={opts.Offset}"; + path = path.SetQueryParam("offset", opts.Offset); return _client.Execute, ListRequestOptions>(path, opts); + + //return _client.Execute, ListRequestOptions>(path, opts, Method.GET, Enums.RequestType.List); + + //opts = opts ?? new ListRequestOptions(); + + //string path = GetRoute("companies", "paged"); + + //path += $"{QueryParams.COUNT}={opts.Limit}"; + + //if (opts.PropertiesToInclude.Any()) + // path += $"{QueryParams.PROPERTIES}={opts.PropertiesToInclude}"; + + //if (opts.Offset.HasValue) + // path += $"{QueryParams.OFFSET}={opts.Offset}"; + + //return _client.Execute, ListRequestOptions>(path, opts); } /// diff --git a/HubSpot.NET/Api/Shared/Associations.cs b/HubSpot.NET/Api/Shared/Associations.cs new file mode 100644 index 00000000..cd9718ba --- /dev/null +++ b/HubSpot.NET/Api/Shared/Associations.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; + +namespace HubSpot.NET.Api.Shared +{ + [DataContract] + public class Associations + { + [DataMember(Name = "associatedCompanyIds")] + public List AssociatedCompanyIds { get; set; } + [DataMember(Name = "associatedVids")] + public List AssociatedVids { get; set; } + } +} diff --git a/HubSpot.NET/Core/Requests/RequestDataConverter.cs b/HubSpot.NET/Core/Requests/RequestDataConverter.cs new file mode 100644 index 00000000..dc4a25fd --- /dev/null +++ b/HubSpot.NET/Core/Requests/RequestDataConverter.cs @@ -0,0 +1,318 @@ +using HubSpot.NET.Api.Shared; +using HubSpot.NET.Core.Extensions; +using HubSpot.NET.Core.Interfaces; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace HubSpot.NET.Core.Requests +{ + public class RequestDataConverter + { + + //Methods + /// + /// Converts a single "dynamic" representation of an entity into a typed entity + /// + /// + /// The dynamic object being passed in should have a prop called "properties" which contains all the object properties to map, as well + /// as vid and other root level objects stored in the HubSpot JSON response + /// + /// An instance that contains a single HubSpot entity to deserialize + /// An instantiated DTO that shall recieve data + /// The populated DTO + internal object ConvertSingleEntity(ExpandoObject dynamicObject, object dto) + { + var expandoDict = (IDictionary)dynamicObject; + var dtoProps = dto.GetType().GetProperties(); + + // The vid is the "id" of the entity + if (expandoDict.TryGetValue("vid", out var vidData)) + { + // TODO use properly serialized name of prop to find it + var vidProp = dtoProps.SingleOrDefault(q => q.GetPropSerializedName() == "vid"); + vidProp?.SetValue(dto, vidData); + } + + // The dealId is the "id" of the deal entity + if (expandoDict.TryGetValue("dealId", out var dealIdData)) + { + // TODO use properly serialized name of prop to find it + var dealIdProp = dtoProps.SingleOrDefault(q => q.GetPropSerializedName() == "dealId"); + dealIdProp?.SetValue(dto, dealIdData); + } + + // The companyId is the "id" of the company entity + if (expandoDict.TryGetValue("companyId", out var companyIdData)) + { + // TODO use properly serialized name of prop to find it + var companyIdProp = dtoProps.SingleOrDefault(q => q.GetPropSerializedName() == "companyId"); + companyIdProp?.SetValue(dto, companyIdData); + } + + // The Properties objec in the json / response data contains all the props we wish to map - if that does not exist + // we cannot proceed + if (!expandoDict.TryGetValue("properties", out var dynamicProperties)) return dto; + + foreach (var dynamicProp in dynamicProperties as ExpandoObject) + { + // prop.Key contains the name of the property we wish to map into the DTO + // prop.Value contains the data returned by HubSpot, which is also an object + // in there we need to go get the "value" prop to get the actual value + + if (!((IDictionary)dynamicProp.Value).TryGetValue("value", out object dynamicValue)) + { + continue; + } + + // TODO use properly serialized name of prop to find and set it's value + try + { + var targetProp = dtoProps.SingleOrDefault(q => q.GetPropSerializedName() == dynamicProp.Key); + if (targetProp != null) + { + Type t = Nullable.GetUnderlyingType(targetProp.PropertyType) ?? targetProp.PropertyType; + + var value = t.IsValueType ? Activator.CreateInstance(t) : null; + + if (!string.IsNullOrEmpty(dynamicValue.ToString())) + { + if (t == typeof(System.Decimal)) { + decimal number = 0; + var style = NumberStyles.AllowDecimalPoint; + var culture = CultureInfo.CreateSpecificCulture("fr-FR"); + if (Decimal.TryParse(dynamicValue.ToString(), style, culture, out number)) + value = number; + else + value = Math.Round(Decimal.Parse(dynamicValue.ToString(), new NumberFormatInfo() { NumberDecimalSeparator = "." }), 4); + + } + else value = Convert.ChangeType(dynamicValue, t); + } + + targetProp?.SetValue(dto, dynamicValue.GetType() == targetProp.PropertyType ? dynamicValue : value); + } + } + catch (Exception ex) + { + Console.WriteLine($"ConvertSingleEntity, ERROR : {ex}"); + } + + } + + if (!expandoDict.TryGetValue("associations", out var dynamicAssociations)) return dto; + foreach (var dynamicAssoc in dynamicAssociations as ExpandoObject) + { + var value = dynamicAssoc.Value as List; + + if (dynamicAssoc.Key != "associatedCompanyIds" && dynamicAssoc.Key != "associatedVids" + && dynamicAssoc.Key != "associatedDealIds") + { + continue; + } + + // TODO use properly serialized name of prop to find and set it's value + var targetProp = dtoProps.SingleOrDefault(q => q.GetPropSerializedName() == dynamicAssoc.Key); + targetProp?.SetValue(dto, value.OfType().ToList()); + + } + return dto; + } + + public T FromHubSpotListResponse(ExpandoObject dynamicObject) where T : IHubSpotModel, new() + { + // get a handle to the underlying dictionary values of te ExpandoObject + var expandoDict = (IDictionary)dynamicObject; + + // For LIST contacts the "contacts" property should be populated, for LIST companies the "companies" property should be populated, and so on + // in our T item, search for a property that is an IList and use that as our prop name selector into the DynamoObject.... + // So on the IContactListHubSpotModel whe have a IList Contacts - find that prop, lowercase to contacts and that prop should + // be in the DynamoObject from HubSpot! Tricky stuff + var targetType = typeof(IHubSpotModel); + var data = new T(); + var dataProps = data.GetType().GetProperties(); + var dataTargetProp = dataProps.SingleOrDefault(p => targetType.IsAssignableFrom(p.PropertyType.GenericTypeArguments.FirstOrDefault())); + + if (dataTargetProp == null) + { + throw new ArgumentException("Unable to locate a property on the data class that implements IList where T is a IHubSpotModel"); + } + + var propSerializedName = dataTargetProp.GetPropSerializedName(); + if (!expandoDict.ContainsKey(propSerializedName)) + { + throw new ArgumentException($"The json data does not contain a property of name {propSerializedName} which is required to decode the json data"); + } + + // Find the generic type for the List in question + var genericEntityType = dataTargetProp.PropertyType.GenericTypeArguments.First(); + // get a handle to Add on the list (actually from ICollection) + var listAddMethod = dataTargetProp.PropertyType.FindMethodRecursively("Add", genericEntityType); + // Condensed version of : https://stackoverflow.com/a/4194063/1662254 + var listInstance = Activator.CreateInstance(typeof(List<>).MakeGenericType(genericEntityType)); + if (listAddMethod == null) + { + throw new ArgumentException("Unable to locat Add method on the list of items to deserialize into - is it an IList ?"); + } + + // Convert all the entities + var jsonEntities = expandoDict[propSerializedName]; + foreach (var entry in jsonEntities as List) + { + // convert single entity + var expandoEntry = entry as ExpandoObject; + var dto = ConvertSingleEntity(expandoEntry, Activator.CreateInstance(genericEntityType)); + // add entity to list + listAddMethod.Invoke(listInstance, new[] { dto }); + } + // assign our reflected list instance onto the data object + dataTargetProp.SetValue(data, listInstance); + var allPropNamesInSerializedFormat = GetAllPropsWithSerializedNameAsKey(dataProps); + // Now try to map any remaining props from the dynamo object into the response dto we shall return + foreach (var kvp in expandoDict) + { + // skip the property with all the items for the response as we have already mapped that + if (kvp.Key == propSerializedName) continue; + + // The Key of the currend item should be mapped, so we have to find a property in the target dto that "Serializes" into this value... + if (!allPropNamesInSerializedFormat.TryGetValue(kvp.Key, out PropertyInfo theProp)) + { + continue; + } + // whe have a property which name serializes to the kvp.Key, let's set the data + + // If theProp is a complex type we canot just use SetValue, we need a conversion + if (theProp.PropertyType.IsComplexType()) + { + var expandoEntry = kvp.Value as ExpandoObject; + var dto = ConvertSingleEntity(expandoEntry, Activator.CreateInstance(theProp.PropertyType)); + theProp.SetValue(data, dto); + } + else // simple value type, assign it + { + theProp.SetValue(data, kvp.Value); + } + } + return data; + } + + /// + /// Convert from the dynamicly typed into a strongly typed + /// + /// The representation of the returned json + /// + public T FromHubSpotResponse(ExpandoObject dynamicObject) where T : IHubSpotModel, new() => + ((T)this.ConvertSingleEntity(dynamicObject, new T())); + + private IDictionary GetAllPropsWithSerializedNameAsKey(PropertyInfo[] dataProps) + { + var dictionary = new Dictionary(); + foreach (var prop in dataProps) + { + var propName = prop.GetPropSerializedName(); + dictionary.Add(propName, prop); + } + return dictionary; + } + + /// + /// Converts the given to a hubspot data entity + /// + /// The entity. + /// + public dynamic ToHubSpotDataEntity(IHubSpotModel entity) + { + dynamic mapped = new ExpandoObject(); + + /* HSCompanyId */ + var hsCompanyId = entity.GetType().GetProperties() + .FirstOrDefault(x => x.Name == "HSCompanyId")?.GetValue(entity); + + if (hsCompanyId != null) + mapped.ObjectId = hsCompanyId; + + /* HSContactId */ + var hsContactId = entity.GetType().GetProperties() + .FirstOrDefault(x => x.Name == "HSContactId")?.GetValue(entity); + + if (hsContactId != null) + mapped.Vid = hsContactId; + + /* HSDealId */ + var hsDealId = entity.GetType().GetProperties() + .FirstOrDefault(x => x.Name == "HSDealId")?.GetValue(entity); + + if (hsDealId != null) + mapped.ObjectId = hsDealId; + + /* associatedCompanyIds */ + var hsAssociatedCompanyIds = entity.GetType().GetProperties() + .FirstOrDefault(x => x.Name == "associatedCompanyIds")?.GetValue(entity); + + var hsAssociatedVids = entity.GetType().GetProperties() + .FirstOrDefault(x => x.Name == "associatedVids")?.GetValue(entity); + + if(hsAssociatedCompanyIds != null || hsAssociatedVids != null) { + mapped.Associations = new Associations(); + + if (hsAssociatedCompanyIds != null) + { + var v = new List(); + var propValue = (List)entity.GetType().GetProperties() + .FirstOrDefault(x => x.Name == "associatedCompanyIds")?.GetValue(entity); + mapped.Associations.AssociatedCompanyIds = propValue; + } + + if (hsAssociatedVids != null) + { + var propValue = (List)entity.GetType().GetProperties() + .FirstOrDefault(x => x.Name == "associatedVids")?.GetValue(entity); + mapped.Associations.AssociatedVids = propValue; + } + + } + + /* Properties */ + mapped.Properties = new List(); + + var allProps = entity.GetType().GetProperties(); + foreach (var prop in allProps) + { + if (prop.HasIgnoreDataMemberAttribute()) { continue; } + var propSerializedName = prop.GetPropSerializedName(); + if (prop.Name.Equals("HSCompanyId") + || prop.Name.Equals("HSDealId") + || prop.Name.Equals("RouteBasePath") + || prop.Name.Equals("IsNameValue") + || prop.Name.Equals("associatedCompanyIds") + || prop.Name.Equals("associatedVids") + || prop.Name.Equals("associatedDealIds") + || prop.Name.StartsWith("hs_")) + { continue; } + // IF we have an complex type on the entity that we are trying to convert, let's NOT get the + // string value of it, but simply pass the object along - it will be serialized later as JSON... + var propValue = prop.GetValue(entity); + var value = propValue.IsComplexType() ? propValue : propValue?.ToString(); + var item = new HubspotDataEntityProp + { + Property = propSerializedName, + Value = value + }; + if (entity.IsNameValue) + { + item.Property = null; + item.Name = propSerializedName; + } + if (item.Value == null) { continue; } + mapped.Properties.Add(item); + } + return mapped; + } + } +} diff --git a/HubSpot.NET/Core/Requests/RequestSerializer.cs b/HubSpot.NET/Core/Requests/RequestSerializer.cs new file mode 100644 index 00000000..d1f57bec --- /dev/null +++ b/HubSpot.NET/Core/Requests/RequestSerializer.cs @@ -0,0 +1,97 @@ +using HubSpot.NET.Core.Interfaces; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Text; + +namespace HubSpot.NET.Core.Requests +{ + public class RequestSerializer + { + //Fields + private readonly RequestDataConverter _requestDataConverter; + private readonly JsonSerializerSettings _jsonSerializerSettings; + + /// + /// Initializes a new instance of the class. + /// + protected RequestSerializer() + { + _jsonSerializerSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }; + } + + /// + /// Initializes a new instance of the class. + /// + /// Use this constructor if you wish to override dependencies + /// The request data converter. + public RequestSerializer(RequestDataConverter requestDataConverter) : this() + { + this._requestDataConverter = requestDataConverter; + } + + /// + /// Deserialize the given JSON into a + /// + /// The json data returned by HubSpot tha should be converted + /// The deserialized entity + public virtual IHubSpotModel DeserializeEntity(string json) where T : IHubSpotModel, new() + { + var jobj = JsonConvert.DeserializeObject(json); + var converted = this._requestDataConverter.FromHubSpotResponse(jobj); + //converted.FromHubSpotDataEntity(jobj); + + return converted; + } + + /// + /// Deserialize the given JSON from a List request into a + /// + /// The JSON data returned from a List request to HubSpot + /// + public virtual IHubSpotModel DeserializeListEntity(string json) where T : IHubSpotModel, new() + { + var expandoObject = JsonConvert.DeserializeObject(json); + var converted = this._requestDataConverter.FromHubSpotListResponse(expandoObject); + + return converted; + } + + /// + /// Serializez the entity to JSON. + /// + /// The entity. + /// The serialized entity + public virtual string SerializeEntity(object obj) + { + if (obj is IHubSpotModel entity) + { + var converted = _requestDataConverter.ToHubSpotDataEntity(entity); + //entity.ToHubSpotDataEntity(ref converted); + return JsonConvert.SerializeObject(converted, _jsonSerializerSettings); + } + return JsonConvert.SerializeObject(obj, _jsonSerializerSettings); + } + + public virtual string SerializeBatchEntity(ICollection obj) where T : IHubSpotModel,new() + { + var result = new List(); + foreach(var i in obj) + { + var converted = _requestDataConverter.ToHubSpotDataEntity(i); + //entity.ToHubSpotDataEntity(ref converted); + result.Add(converted); + } + var test = JsonConvert.SerializeObject(result, _jsonSerializerSettings); + + return JsonConvert.SerializeObject(result, _jsonSerializerSettings); + } + } +} diff --git a/HubSpot.NET/HubSpot.NET.csproj b/HubSpot.NET/HubSpot.NET.csproj index 8a3a3fb9..9d1e7573 100644 --- a/HubSpot.NET/HubSpot.NET.csproj +++ b/HubSpot.NET/HubSpot.NET.csproj @@ -22,6 +22,8 @@ 7.3 + + @@ -29,7 +31,6 @@ -