Skip to content

Instantly share code, notes, and snippets.

Created July 24, 2017 12:55
Show Gist options
  • Save anonymous/82055eb397fc65068f9987df7160df0a to your computer and use it in GitHub Desktop.
Save anonymous/82055eb397fc65068f9987df7160df0a to your computer and use it in GitHub Desktop.
Serialize/Deserialize complex objects into string fields in Solr
using System.Collections.Generic;
public class SearchResponse<T>
{
public int NumFound { get; set; }
public int Start { get; set; }
public IEnumerable<T> Docs { get; set; }
}
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
public class SolrClient
{
public SolrClient(string solrUrl)
{
this.solrUrl = solrUrl;
}
public async Task<SearchResponse<T>> Search<T>(string pathAndQuery)
{
var settings = new JsonSerializerSettings { ContractResolver = new SolrContractResolver(SolrContractResolverAction.Deserialize) };
var serializer = JsonSerializer.Create(settings);
using (var http = SolrHttpClient(solrUrl))
{
var response = await http.GetAsync(pathAndQuery).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
throw new Exception(await response.Content.ReadAsStringAsync());
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JObject.Parse(content)["response"].ToObject<SearchResponse<T>>(serializer);
}
}
public async Task Execute(string pathAndQuery)
{
using (var http = SolrHttpClient(solrUrl))
{
var response = await http.GetAsync(pathAndQuery);
if (!response.IsSuccessStatusCode)
throw new Exception(await response.Content.ReadAsStringAsync());
}
}
public async Task Delete(string index, Commit commit, string query)
{
var json = JsonConvert.SerializeObject(new { delete = new { query } });
using (var http = SolrHttpClient(solrUrl))
{
var response = await http.PostAsync($"{index}/update/json?commit={commit.ToLower()}", new StringContent(json));
if (!response.IsSuccessStatusCode)
throw new Exception(await response.Content.ReadAsStringAsync());
}
}
public async Task Delete<T>(string index, Commit commit, IEnumerable<T> ids)
{
if (ids == null)
return;
var ids_ = ids.ToList();
if (!ids_.Any())
return;
var json = JsonConvert.SerializeObject(new { delete = ids_ });
using (var http = SolrHttpClient(solrUrl))
{
var response = await http.PostAsync($"{index}/update/json?commit={commit.ToLower()}", new StringContent(json));
if (!response.IsSuccessStatusCode)
throw new Exception(await response.Content.ReadAsStringAsync());
}
}
public async Task Update<T>(string index, Commit commit, IEnumerable<T> entries)
{
if (entries == null)
return;
var entries_ = entries.ToList();
if (!entries_.Any())
return;
var settings = new JsonSerializerSettings {
ContractResolver = new SolrContractResolver(SolrContractResolverAction.Serialize),
NullValueHandling = NullValueHandling.Ignore
};
var json = JsonConvert.SerializeObject(entries_, settings);
using (var http = SolrHttpClient(solrUrl))
{
var response = await http.PostAsync($"{index}/update/json?commit={commit.ToLower()}", new StringContent(json));
if (!response.IsSuccessStatusCode)
throw new Exception(await response.Content.ReadAsStringAsync());
}
}
string solrUrl;
HttpClient SolrHttpClient(string solrUrl) => new HttpClient { BaseAddress = new Uri(solrUrl + "solr/") };
}
public enum Commit
{
True,
False
}
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
public enum SolrContractResolverAction
{
Serialize,
Deserialize
}
/// <summary>
/// Transforms complex objects into something Solr understands (raw strings) and back again (C# objects).
/// </summary>
public class SolrContractResolver : DefaultContractResolver
{
SolrContractResolverAction action;
public SolrContractResolver(SolrContractResolverAction action)
{
this.action = action;
}
// Solr supports conversion of simple types when dealing with json. If you find a type Solr supports without
// custom conversion, feel free to add it here.
HashSet<Type> typesNotRequiredToConvert = new HashSet<Type>
{
typeof(bool),
typeof(int),
typeof(long),
typeof(float),
typeof(double),
typeof(decimal),
typeof(string),
typeof(DateTime),
typeof(Guid)
};
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
var propertyInfo = property.DeclaringType.GetProperty(property.UnderlyingName);
if (propertyInfo == null)
throw new Exception("Fields are unsupported. Please only use properties or feel free to add support for fields.");
var propertyType = propertyInfo.PropertyType;
if (propertyType.FullName.StartsWith("System.Nullable`1"))
// No custom logic for Nullables. You're safe as long you adhere to the types in 'typesNotRequiredToConvert'.
return property;
if (propertyType.IsEnum)
return property;
if (typesNotRequiredToConvert.Contains(propertyType))
return property;
if (IsCollection(propertyInfo) && typesNotRequiredToConvert.Contains(GetItemType(propertyType)))
return property;
if (action == SolrContractResolverAction.Serialize)
// Object needs to be converted into a json string
property.ValueProvider = new ObjectToJsonStringValueProvider(propertyInfo);
else if (action == SolrContractResolverAction.Deserialize)
// Object needs to be converted from a json string back to an object
property.MemberConverter = new JsonStringToObjectConverter();
return property;
}
bool IsCollection(PropertyInfo propertyInfo) =>
propertyInfo.PropertyType.GetInterface(typeof(IEnumerable<>).FullName) != null ||
propertyInfo.PropertyType.GetInterface(typeof(IEnumerable).FullName) != null;
/// <summary>
/// Get item type for arrays/lists. Returns null if no item type is found
/// </summary>
Type GetItemType(Type type)
{
if (type.IsArray)
return type.GetElementType();
// type is IEnumerable<T>;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return type.GetGenericArguments()[0];
// type implements/extends IEnumerable<T>;
return type.GetInterfaces()
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.Select(t => t.GenericTypeArguments[0]).FirstOrDefault();
}
}
class ObjectToJsonStringValueProvider : IValueProvider
{
PropertyInfo propertyInfo;
public ObjectToJsonStringValueProvider(PropertyInfo propertyInfo)
{
this.propertyInfo = propertyInfo;
}
public object GetValue(object target)
{
var value = propertyInfo.GetValue(target);
if (value == null)
return null;
// Turn complex object into a raw json string, since Solr doesn't support nested objects (it does, but in a weird way)
return JsonConvert.SerializeObject(value).ToString();
}
public void SetValue(object target, object value)
{
throw new NotImplementedException();
}
}
class JsonStringToObjectConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type type, object _, JsonSerializer serializer) =>
// Solr returns nested json as a raw string, but one can apply a json transformer, turning the result into actual json
// Both scenarios are supported here. https://cwiki.apache.org/confluence/display/solr/Transforming+Result+Documents
reader.TokenType == JsonToken.String
? JsonConvert.DeserializeObject(reader.Value.ToString(), type)
: JToken.Load(reader).ToObject(type, serializer);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer _)
{
throw new NotImplementedException();
}
public override bool CanWrite => false;
public override bool CanRead => true;
public override bool CanConvert(Type _) => true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment