Simplest way to flatten document to a view in RavenDB - c#

Given the following classes:
public class Lookup
{
public string Code { get; set; }
public string Name { get; set; }
}
public class DocA
{
public string Id { get; set; }
public string Name { get; set; }
public Lookup Currency { get; set; }
}
public class ViewA // Simply a flattened version of the doc
{
public string Id { get; set; }
public string Name { get; set; }
public string CurrencyName { get; set; } // View just gets the name of the currency
}
I can create an index that allows client to query the view as follows:
public class A_View : AbstractIndexCreationTask<DocA, ViewA>
{
public A_View()
{
Map = docs => from doc in docs
select new ViewA
{
Id = doc.Id,
Name = doc.Name,
CurrencyName = doc.Currency.Name
};
Reduce = results => from result in results
group on new ViewA
{
Id = result.Id,
Name = result.Name,
CurrencyName = result.CurrencyName
} into g
select new ViewA
{
Id = g.Key.Id,
Name = g.Key.Name,
CurrencyName = g.Key.CurrencyName
};
}
}
This certainly works and produces the desired result of a view with the data transformed to the structure required at the client application. However, it is unworkably verbose, will be a maintenance nightmare and is probably fairly inefficient with all the redundant object construction.
Is there a simpler way of creating an index with the required structure (ViewA) given a collection of documents (DocA)?
FURTHER INFORMATION
The issue appears to be that in order to have the index hold the data in the transformed structure (ViewA), we have to do a Reduce. It appears that a Reduce must have both a GROUP ON and a SELECT in order to work as expected so the following are not valid:
INVALID REDUCE CLAUSE 1:
Reduce = results => from result in results
group on new ViewA
{
Id = result.Id,
Name = result.Name,
CurrencyName = result.CurrencyName
} into g
select g.Key;
This produces: System.InvalidOperationException: Variable initializer select must have a lambda expression with an object create expression
Clearly we need to have the 'select new'.
INVALID REDUCE CLAUSE 2:
Reduce = results => from result in results
select new ViewA
{
Id = result.Id,
Name = result.Name,
CurrencyName = result.CurrencyName
};
This prduces: System.InvalidCastException: Unable to cast object of type 'ICSharpCode.NRefactory.Ast.IdentifierExpression' to type 'ICSharpCode.NRefactory.Ast.InvocationExpression'.
Clearly, we also need to have the 'group on new'.
Thanks for any assistance you can provide.
(Note: removing the type (ViewA) from the constructor calls has no effect on the above)
UPDATE WITH CORRECT APPROACH
As outlined in Daniel's blog mentioned in the answer below, here is the correct way to do this for this example:
public class A_View : AbstractIndexCreationTask<DocA, ViewA>
{
public A_View()
{
Map = docs => from doc in docs
select new ViewA
{
Id = doc.Id,
Name = doc.Name,
CurrencyName = doc.Currency.Name
};
// Top-level properties on ViewA that match those on DocA
// do not need to be stored in the index.
Store(x => x.CurrencyName, FieldStorage.Yes);
}
}

One solution, simply flatten in the Map and configure the index to store only properties that do not exist in DocA.
public class A_View : AbstractIndexCreationTask<DocA, ViewA>
{
public A_View()
{
Map = docs => from doc in docs
select new ViewA
{
Id = doc.Id,
Name = doc.Name,
CurrencyName = doc.Currency.Name
};
// Top-level properties on ViewA that match those on DocA
// do not need to be stored in the index.
Store(x => x.CurrencyName, FieldStorage.Yes);
}
}

Related

Custom mapping in Dapper

I'm attempting to use a CTE with Dapper and multi-mapping to get paged results. I'm hitting an inconvenience with duplicate columns; the CTE is preventing me from having to Name columns for example.
I would like to map the following query onto the following objects, not the mismatch between the column names and properties.
Query:
WITH TempSites AS(
SELECT
[S].[SiteID],
[S].[Name] AS [SiteName],
[S].[Description],
[L].[LocationID],
[L].[Name] AS [LocationName],
[L].[Description] AS [LocationDescription],
[L].[SiteID] AS [LocationSiteID],
[L].[ReportingID]
FROM (
SELECT * FROM [dbo].[Sites] [1_S]
WHERE [1_S].[StatusID] = 0
ORDER BY [1_S].[Name]
OFFSET 10 * (1 - 1) ROWS
FETCH NEXT 10 ROWS ONLY
) S
LEFT JOIN [dbo].[Locations] [L] ON [S].[SiteID] = [L].[SiteID]
),
MaxItems AS (SELECT COUNT(SiteID) AS MaxItems FROM Sites)
SELECT *
FROM TempSites, MaxItems
Objects:
public class Site
{
public int SiteID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<Location> Locations { get; internal set; }
}
public class Location
{
public int LocationID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public Guid ReportingID { get; set; }
public int SiteID { get; set; }
}
For some reason I have it in my head that a naming convention exists which will handle this scenario for me but I can't find mention of it in the docs.
There are more than one issues, let cover them one by one.
CTE duplicate column names:
CTE does not allow duplicate column names, so you have to resolve them using aliases, preferably using some naming convention like in your query attempt.
For some reason I have it in my head that a naming convention exists which will handle this scenario for me but I can't find mention of it in the docs.
You probably had in mind setting the DefaultTypeMap.MatchNamesWithUnderscores property to true, but as code documentation of the property states:
Should column names like User_Id be allowed to match properties/fields like UserId?
apparently this is not the solution. But the issue can easily be solved by introducing a custom naming convention, for instance "{prefix}{propertyName}" (where by default prefix is "{className}_") and implementing it via Dapper's CustomPropertyTypeMap. Here is a helper method which does that:
public static class CustomNameMap
{
public static void SetFor<T>(string prefix = null)
{
if (prefix == null) prefix = typeof(T).Name + "_";
var typeMap = new CustomPropertyTypeMap(typeof(T), (type, name) =>
{
if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
name = name.Substring(prefix.Length);
return type.GetProperty(name);
});
SqlMapper.SetTypeMap(typeof(T), typeMap);
}
}
Now all you need is to call it (one time):
CustomNameMap.SetFor<Location>();
apply the naming convention to your query:
WITH TempSites AS(
SELECT
[S].[SiteID],
[S].[Name],
[S].[Description],
[L].[LocationID],
[L].[Name] AS [Location_Name],
[L].[Description] AS [Location_Description],
[L].[SiteID] AS [Location_SiteID],
[L].[ReportingID]
FROM (
SELECT * FROM [dbo].[Sites] [1_S]
WHERE [1_S].[StatusID] = 0
ORDER BY [1_S].[Name]
OFFSET 10 * (1 - 1) ROWS
FETCH NEXT 10 ROWS ONLY
) S
LEFT JOIN [dbo].[Locations] [L] ON [S].[SiteID] = [L].[SiteID]
),
MaxItems AS (SELECT COUNT(SiteID) AS MaxItems FROM Sites)
SELECT *
FROM TempSites, MaxItems
and you are done with that part. Of course you can use shorter prefix like "Loc_" if you like.
Mapping the query result to the provided classes:
In this particular case you need to use the Query method overload that allows you to pass Func<TFirst, TSecond, TReturn> map delegate and unitilize the splitOn parameter to specify LocationID as a split column. However that's not enough. Dapper's Multi Mapping feature allows you to split a single row to a several single objects (like LINQ Join) while you need a Site with Location list (like LINQ GroupJoin).
It can be achieved by using the Query method to project into a temporary anonymous type and then use regular LINQ to produce the desired output like this:
var sites = cn.Query(sql, (Site site, Location loc) => new { site, loc }, splitOn: "LocationID")
.GroupBy(e => e.site.SiteID)
.Select(g =>
{
var site = g.First().site;
site.Locations = g.Select(e => e.loc).Where(loc => loc != null).ToList();
return site;
})
.ToList();
where cn is opened SqlConnection and sql is a string holding the above query.
You can map a column name with another attribute using the ColumnAttributeTypeMapper.
See my first comment on the Gist for further details.
You can do the mapping like
public class Site
{
public int SiteID { get; set; }
[Column("SiteName")]
public string Name { get; set; }
public string Description { get; set; }
public List<Location> Locations { get; internal set; }
}
public class Location
{
public int LocationID { get; set; }
[Column("LocationName")]
public string Name { get; set; }
[Column("LocationDescription")]
public string Description { get; set; }
public Guid ReportingID { get; set; }
[Column("LocationSiteID")]
public int SiteID { get; set; }
}
Mapping can be done using either of the following 3 methods
Method 1
Manually set the custom TypeMapper for your Model once as:
Dapper.SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
Dapper.SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
Method 2
For class libraries of .NET Framework >= v4.0, you can use PreApplicationStartMethod to register your classes for custom type mapping.
using System.Web;
using Dapper;
[assembly: PreApplicationStartMethod(typeof(YourNamespace.Initiator), "RegisterModels")]
namespace YourNamespace
{
public class Initiator
{
private static void RegisterModels()
{
SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
// ...
}
}
}
Method 3
Or you can find the classes to which ColumnAttribute is applied through reflection and set type mappings. This could be a little slower, but it does all the mappings in your assembly automatically for you. Just call RegisterTypeMaps() once your assembly is loaded.
public static void RegisterTypeMaps()
{
var mappedTypes = Assembly.GetAssembly(typeof (Initiator)).GetTypes().Where(
f =>
f.GetProperties().Any(
p =>
p.GetCustomAttributes(false).Any(
a => a.GetType().Name == ColumnAttributeTypeMapper<dynamic>.ColumnAttributeName)));
var mapper = typeof(ColumnAttributeTypeMapper<>);
foreach (var mappedType in mappedTypes)
{
var genericType = mapper.MakeGenericType(new[] { mappedType });
SqlMapper.SetTypeMap(mappedType, Activator.CreateInstance(genericType) as SqlMapper.ITypeMap);
}
}
The below code should work fine for you to load a list of sites with associated locations
var conString="your database connection string here";
using (var conn = new SqlConnection(conString))
{
conn.Open();
string qry = "SELECT S.SiteId, S.Name, S.Description, L.LocationId, L.Name,L.Description,
L.ReportingId
from Site S INNER JOIN
Location L ON S.SiteId=L.SiteId";
var sites = conn.Query<Site, Location, Site>
(qry, (site, loc) => { site.Locations = loc; return site; });
var siteCount = sites.Count();
foreach (Site site in sites)
{
//do something
}
conn.Close();
}

LINQ Query (Group BY) one column

Consider the following classes
public class DashboardTile
{
public int ID { get; set; }
public int? CategoryID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class DashboardTileBO : DashboardTile
{
public bool IsChecked { get; set; }
public List<DashboardTileBO> DashboardTiles { get; set; }
}
I have list of tiles in which some tiles are child of other.Now I want to show my list of tiles in such a way that if it has childs it gets added to the list.
query I am trying
var allDashBoardTiles = (from a in context.DashboardTiles
group a by a.CategoryID into b
select new BusinessObjects.DashboardTileBO
{
ID = a.ID,
Name = a.Name,
Description = b.Description,
DashboardTiles = b.ToList(),
}).ToList();
var list = context.DashboardUserTiles.Where(a => a.UserID == userId).Select(a => a.DashboardTileID).ToList();
allDashBoardTiles.ForEach(a => a.IsChecked = list.Contains(a.ID));
Now in above query when I use group clause and in select if I use a.ID,a.Name etc it says that it doesnot contain definitionor extension method for it.
Table
You can't access the properties of a directly because GroupBy returns IGrouping<TKey,T>. You can include other columns also in your group by and access them like this:-
(from a in context.DashboardTiles
group a by new { a.CategoryID, a.ID, a.Name } into b
select new BusinessObjects.DashboardTileBO
{
ID = b.Key.ID,
Name = b.Key.Name,
DashboardTiles = b.ToList(),
}).ToList();
Edit:
Also, I guess the property DashboardTiles in DashboardTileBO class should be List<DashboardTile> instead of List<DashboardTileBO>, otherwise we cannot fetch it from DashboardTiles data.

Linq select many into new column

I am trying to make a jquery auto complete where I use a label and value like in this post which means that I need my json in the form of
{ label: 'label text', value: 'value text' }
However I am filtering a list of Employees which is a class with the following structure:
public sealed class Employee
{
public string Name { get; set; }
public string PersonnelNumber { get; set; }
public int RecID { get; set; }
public string Email { get; set; }
}
So I tried the following Linq to get the format of label, value I needed:
var jsonResult = employees
.SelectMany(emp => new { label = emp.Name, value = emp.RecID })
.ToList();
Where employees is a list of Employee objects but it is throwing up an build error of
Error 1 The type arguments for method 'System.Linq.Enumerable.SelectMany(System.Collections.Generic.IEnumerable, System.Func>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
How do I fix this to get the Name and RecID in a new list of objects with label and value as their output?
I think you just want to use Select here:
var jsonResult = employees
.Select(emp => new { label = emp.Name, value = emp.RecID })
.ToList();
SelectMany is for "flattening" a group of collections. Since you just have a single collection just use Select:
var jsonResult = employees.Select(emp => new { label = emp.Name, value = emp.RecID })
.ToList();

LINQ Sum list of items grouped by type inside list

I have the following order object which contains a list of order addons. I am trying to create a report that shows all the addon types and their quantities summed.
public class Order {
public IList<OrderAddon> OrderAddons { get; set; }
}
public class OrderAddon {
public enum OrderType { get; set; }
public string Name { get; set; }
public int Quantity { get; set; }
}
This is where I am at and can't figure out if the entire query is wrong of I am just missing something.
var query = from order in Model.Orders
from addon in order.OrderAddons
group order by addon.AddonType
into orderAddons select new
{
Name = orderAddons.Key,
Quantity = orderAddons.Sum(x => x.) << This is where I am stuck
};
When I hit . my intellisense is showing me properties in order object not the addon object.
That's because you're saying group order by ..., so the orderAddons object becomes a grouping of orders. You can use this if you're going to need properties from both objects:
from order in Model.Orders
from addon in order.OrderAddons
group new{addon, order} by addon.AddonType
into orderAddons select new
{
Name = orderAddons.Key,
Quantity = orderAddons.Sum(x => x.addon.Quantity)
};
If this is all the data you need, this is a little simpler:
from order in Model.Orders
from addon in order.OrderAddons
group order.Quantity by addon.AddonType
into quantityByAddonType select new
{
Name = quantityByAddonType.Key,
Quantity = quantityByAddonType.Sum()
};
an alternative syntax same result...
var result = Model.Orders
.SelectMany(order => order.OrderAddons)
.GroupBy(addon => addon.OrderType)
.Select(grouping => new
{
Name = grouping.Key,
Quantity = grouping.Sum(addon => addon.Quantity)
});

RavenDB cross entity query matching on non-identifier property

I have the following entity collections in RavenDB:
public class EntityA
{
public string Id { get; set; }
public string Name { get; set; }
public string[] Tags { get; set; }
}
public class EntityB
{
public string Id { get; set; }
public string Name { get; set; }
public string[] Tags { get; set; }
}
The only thing shared is the Tags collection: a tag of EntityA may exist in EntityB, so that they may intersect.
How can I retrieve every EntityA that has intersecting tags with EntityB where the Name property of EntityB is equal to a given value?
Well, this is a difficult one. To do it right, you would need two levels of reducing - one by the tag which would expand out your results, and another by the id to collapse it back. Raven doesn't have an easy way to do this.
You can fake it out though using a Transform. The only problem is that you will have skipped items in your result set, so make sure you know how to deal with those.
public class TestIndex : AbstractMultiMapIndexCreationTask<TestIndex.Result>
{
public class Result
{
public string[] Ids { get; set; }
public string Name { get; set; }
public string Tag { get; set; }
}
public TestIndex()
{
AddMap<EntityA>(entities => from a in entities
from tag in a.Tags.DefaultIfEmpty("_")
select new
{
Ids = new[] { a.Id },
Name = (string) null,
Tag = tag
});
AddMap<EntityB>(entities => from b in entities
from tag in b.Tags
select new
{
Ids = new string[0],
b.Name,
Tag = tag
});
Reduce = results => from result in results
group result by result.Tag
into g
select new
{
Ids = g.SelectMany(x => x.Ids),
g.First(x => x.Name != null).Name,
Tag = g.Key
};
TransformResults = (database, results) =>
results.SelectMany(x => x.Ids)
.Distinct()
.Select(x => database.Load<EntityA>(x));
}
}
See also the full unit test here.
There is another approach, but I haven't tested it yet. That would be to use the Indexed Properties Bundle to do the first pass, and then map those results for the second pass. I am experimenting with this in general, and if it works, I will update this answer with the results.

Categories