Keep running into "When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id" error for the below code-block:
var accounts = DbConnection.Query<Account, Branch, Application, Account>(
"select Accounts.*, SplitAccount = '', Branches.*, SplitBranch = '', Applications.*" +
" from Accounts" +
" join Branches" +
" on Accounts.BranchId = Branches.BranchId" +
" join Applications" +
" on Accounts.ApplicationId = Applications.ApplicationId" +
" where Accounts.AccountId <> 0",
(account, branch, application) =>
{
account.Branch = branch;
account.Application = application;
return account;
}, splitOn : "SplitAccount, SplitBranch"
).AsQueryable();
I'm using SplitAccount and SplitBranch for splitOn as a workaround.
Em I missing something?
Thanks
Edit:
I have cleaned up my test a little, below is a light version of classes and a new query:
public class AccountLight
{
public int AccountId { get; set; }
public string AccountNumber { get; set; }
public BranchLight Branch { get; set; }
public ApplicationLight Application { get; set; }
}
public class BranchLight
{
public int BranchId { get; set; }
public string BranchNumber { get; set; }
}
public class ApplicationLight
{
public int ApplicationId { get; set; }
public string ApplicationCode { get; set; }
}
var accounts2 = DbConnection.Query<AccountLight, BranchLight, ApplicationLight, AccountLight>(
"select Accounts.AccountId, Accounts.AccountNumber," +
" Branches.BranchId, Branches.BranchNumber," +
" Applications.ApplicationId, Applications.ApplicationCode" +
" from Accounts" +
" inner join Branches" +
" on Accounts.BranchId = Branches.BranchId" +
" inner join Applications" +
" on Accounts.ApplicationId = Applications.ApplicationId" +
" where Accounts.AccountId <> 0",
(account, brach, application) =>
{
account.Branch = brach;
account.Application = application;
return account;
},
commandType: CommandType.Text,
splitOn: "AccountId, BranchId"
).AsQueryable();
After few hours of debugging Dapper's source code, I finally found the issue and it is quite interesting one.
When multiple splitOn fields are supplied, Dapper does a split based on comma, e.g. var splits = splitOn.Split(',').ToArray(). Then it loops through all record-set fields and split’s them up into objects based on the above array; pretty strait forward.
Now the fun part: When I supplied my splitOn fields, I had an extra SPACE after the comma, e.g. “AccountId, BranchId” and that little space was the cause. After Split(), BranchId field contained an extra space and failed to match with ANY fields in the record-set.
There are two ways around this:
Do not use extra spaces after commas; which I personally addicted
to; an old habit from SQL.
Modify Dapper’s GenerateDeserializers
method and change: var currentSplit = splits[splitIndex] to var
currentSplit = splits[splitIndex].Trim(), or something similar; that is what I did for my local copy.
Here is code snapshot:
private static Func<IDataReader, object>[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader)
{
int current = 0;
var splits = splitOn.Split(',').ToArray();
var splitIndex = 0;
Func<Type, int> nextSplit = type =>
{
var currentSplit = splits[splitIndex].Trim();
if (splits.Length > splitIndex + 1)
{
splitIndex++;
}
Update:
The above fix got merged: https://github.com/SamSaffron/dapper-dot-net/commit/399db17e5aa6f1eefaf8fdccff827020be8e6cbb
Related
I have made my query to the database as raw SQL and want to shift to SQL to entities.
Reason for that is performance is terrible in the way it's constructed, not the actual query, which is fast, but afterwards I had to perform multiple SQL statements to get data from associated tables. I was thinking that if I moved to SQL to entities, the associated tables were already fetched and I only had to make that one call to the database.
I construct my old query like this:
List<Database.Product> products = null;
string sql = "SELECT P.* FROM Product p " +
"LEFT JOIN ProductAssigned pa ON pa.ProductId = p.Id " +
"WHERE pa.UserId = #userid AND pa.UnassignedAt IS NULL";
sql = FilterByProductState(productFilter, sql);
if (startDate != default && endDate != default)
{
string start = startDate.Year + "/" + startDate.Month + "/" + startDate.Day;
string end = endDate.Year + "/" + endDate.Month + "/" + endDate.AddDays(1).Day;
sql += " AND (p.StartAt BETWEEN '" + start + "' AND '" + end + "' OR p.CompleteAt BETWEEN '" + start + "' AND '" + end + "')";
}
List<SqlParameter> sqlParameters = new List<SqlParameter>
{
new SqlParameter("#userid", userId)
};
sql += " ORDER BY p.StartAt";
try
{
products = context.Database.SqlQuery<Database.Product>(sql, new SqlParameter("#userid", userId)).ToList();
}
catch (Exception e)
{
throw;
}
The missing method from the code:
private static string FilterByProductState(ProductFilter productFilter, string sql)
{
switch (productFilter)
{
case ProductFilter.Started:
sql += " AND p.StartedAt IS NOT NULL";
sql += " AND p.CompletedAt IS NULL";
sql += " AND p.DeletedAt IS NULL";
break;
case ProductFilter.Closed:
sql += " AND p.CompletedAt IS NOT NULL";
sql += " AND p.DeletedAt IS NULL";
break;
case ProductFilter.AllNotStarted:
sql += " AND p.StartedAt IS NULL";
sql += " AND p.DeletedAt IS NULL";
break;
case ProductFilter.All:
default:
break;
}
return sql;
}
After this like I said I have to go and get certain associated tables.
I have tried to start several things, e.g. using GroupJoin, but nothing seems to pan out. Also I'm creating the SQL statement dynamically, so I don't even know if it could work in SQL to entities.
UPDATE:
Product(the interesting parts) and ProductAssigned is shown here:
public partial class Product
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Product()
{
this.ProductAssigned = new HashSet<ProductAssigned>();
}
public int Id { get; set; }
public string Name { get; set; }
public Nullable<System.DateTime> StartAt { get; set; }
public Nullable<System.DateTime> CompleteAt { get; set; }
public Nullable<System.DateTime> DeletedAt { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<ProductAssigned> ProductAssigned { get; set; }
}
public partial class ProductAssigned
{
public int Id { get; set; }
public int ProductId { get; set; }
public int UserId { get; set; }
public System.DateTime AssignedAt { get; set; }
public Nullable<System.DateTime> UnassignedAt { get; set; }
public virtual Product Product { get; set; }
public virtual User CreatedByUser { get; set; }
public virtual User DeletedByUser { get; set; }
public virtual User UserAssigned { get; set; }
}
The LINQ query can be built in a very similar fashion. Just instead of manual joins, in EF queries you think in objects, references and collections, and EF converts that to the necessary joins.
The first part could be written exactly as the SQL query
IQueryable<Database.Product> query =
from p in context.Products
from pa in p.ProductAssigned.DefaultIfEmpty() // <-- left join
where pa.UserId == userid && pa.UnassignedAt == null
select p;
but it doesn't make much sense. WHERE condition after LEFT JOIN effectively makes it INNER, and also the whole purpose of that join in the original query seems to be to check for existence, so it could be formulated as follows
IQueryable<Database.Product> query = context.Products
.Where(p => p.ProductAssigned.Any(pa =>
pa.UserId == userid && pa.UnassignedAt == null));
Whatever you choose for the first (static) part of the query, the rest of it (the dynamic part) can be built by simply chaining Where calls (at the end they will be combined with AND in the WHERE clause).
query = FilterByProductState(productFilter, query);
if (startDate != default && endDate != default)
{
var start = startDate.Date;
var end = start.AddDays(1);
query = query.Where(p => (p.StartAt >= start && p.StartAt <= end)
|| (p.CompleteAt >= start && p.CompleteAt <= end));
}
query = query.OrderBy(p => p.StartAt);
and the equivalent helper method
private static IQueryable<Database.Product> FilterByProductState(
ProductFilter productFilter,
IQueryable<Database.Product> query)
{
switch (productFilter)
{
case ProductFilter.Started:
query = query.Where(p => p.StartedAt != null
&& p.CompletedAt == null
&& p.DeletedAt == null);
break;
case ProductFilter.Closed:
query = query.Where(p => p.CompletedAt != null
&& p.DeletedAt == null);
break;
case ProductFilter.AllNotStarted:
query = query.Where(p => p.StartedAt == null
&& p.DeletedAt == null);
break;
case ProductFilter.All:
default:
break;
}
return query;
}
Using basic DAL 2 to grab data from a table that has tabid. Would like to also get the Tab Url through the DNN API. I could join to the Tabs table, but want to work with the api.
Here is my model.
[TableName("My_Products")]
[PrimaryKey("ProductId")]
[Cacheable("My_Products_", CacheItemPriority.Normal, 20)]
public class ProductInfo
{
public ProductInfo()
{
Langs = new List<ProductLangInfo>();
}
public int ProductId { get; set; }
[ReadOnlyColumn]
public string ProductImage { get; set; }
public int LineID { get; set; }
[ReadOnlyColumn]
public string Culture { get; set; }
[ReadOnlyColumn]
public string ProductName { get; set; }
[ReadOnlyColumn]
public string ProductShortDesc { get; set; }
[ReadOnlyColumn]
public int TabId { get; set; }
[ReadOnlyColumn]
public string ProductURL { get; set; } //GET THIS FROM API
[ReadOnlyColumn]
public List<ProductLangInfo> Langs { get; set; }
}
This is my Controller
public IEnumerable<ProductInfo> GetProducts(string language)
{
using (IDataContext ctx = DataContext.Instance())
{
string sqlCmd = ";WITH cte as (SELECT * FROM [ProductsLang] WHERE Culture = #0)" +
" SELECT Products.*,cte.ProductName, cte.ProductShortDesc, cte.TabId" +
" FROM [Products] as Products" +
" INNER JOIN cte ON Products.ProductId = cte.ProductId";
string order = " ORDER BY Products.ProductId DESC";
return ctx.ExecuteQuery<ProductInfo>(CommandType.Text, sqlCmd + order, language);
}
}
I guess my question is where is the best way to pass in the tabid from my query to the DNN API?
This should be fairly straightforward using the NavigateURL method off DotNetNuke.Common.Globals
var url = DotNetNuke.Common.Globals.NavigateURL(TabId);
There are other ways to get URLs in DNN, but that is the easiest way that should respect all the various Providers that can be used to build out a URL
private string _productUrl = '';
public string ProductURL { get return DotnetNuke.Common.Globals.NavigateURL(TabId); }
I have the following model:
public class Model {
public string Name { get; set; }
public List<int> Numbers { get; set; }
}
And an SQL query that returns the following dataset containing two nvarchar columns:
Name
Numbers
foo
1,2,3,4
bar
4,17
Is there a simple way to auto-assign the results of the query to a List<Model> using Dapper?
I know I could use multi-mapping and make the splitting myself in C# code, but I would rather get a simpler solution.
I'm not sure if you can call this "simpler", but something like this is an option:
public class Result
{
public string Name { get; set; }
public List<int> Numbers { get; set; }
}
public class DapperTests
{
[Test]
public void Test()
{
var conn = new SqlConnection(#"Data Source=.\sqlexpress; Integrated Security=true; Initial Catalog=mydb");
conn.Open();
var result = conn.Query<string, string, Result>(
"select Name = 'Foo', Numbers = '1,2,3' union all select Name = 'Bar', Numbers = '4,5,6'", (a, b) => new Result
{
Name = a,
Numbers = b.Split(',').Select(Int32.Parse).ToList()
}, splitOn: "*").ToList();
Assert.That(result.Count, Is.EqualTo(2));
Assert.That(result.FirstOrDefault(x => x.Name == "Foo").Numbers.Count, Is.GreaterThan(0));
Assert.That(result.FirstOrDefault(x => x.Name == "Bar").Numbers.Count, Is.GreaterThan(0));
}
}
An alternative option with multimapping... pretty ugly
public class Result
{
public string Name { get; set; }
public List<int> NumberList { get; set; }
public string Numbers { set { NumberList = value.Split(',').Select(Int32.Parse).ToList(); } }
}
public class DapperTests
{
[Test]
public void Test()
{
var conn = new SqlConnection(#"Data Source=.\sqlexpress; Integrated Security=true; Initial Catalog=mydb");
conn.Open();
var sql = #"
select Name = 'Foo', Numbers = '1,2,3';
select Name = 'Bar', Numbers = '4,5,6';";
var expectedResults = 2;
var results = new List<Result>();
using (var multi = conn.QueryMultiple(sql))
{
for (int i = 0; i < expectedResults; i++)
{
results.Add(multi.Read<Result>().Single());
}
}
Assert.That(results.Count, Is.EqualTo(2));
Assert.That(results.FirstOrDefault(x => x.Name == "Foo").NumberList.Count, Is.GreaterThan(0));
Assert.That(results.FirstOrDefault(x => x.Name == "Bar").NumberList.Count, Is.GreaterThan(0));
}
}
Currently, I have model which represents a table within my database.
public partial class Booking
{
public int BookingId { get; set; }
public string BookingFirstname { get; set; }
public string BookingSurname { get; set; }
public string BookingEmail { get; set; }
public string BookingMobileTel { get; set; }
public string BookingHomeTel { get; set; }
public int BookingAge { get; set; }
public int BookingPassengers { get; set; }
public int DepartureId { get; set; }
public int ArrivalId { get; set; }
}
DepartureId and ArrivalId are foreign keys of two other tables.
public partial class Departure
{
public int DepartureId { get; set; }
public string DepartureName { get; set; }
}
public partial class Arrival
{
public int ArrivalId { get; set; }
public int DepartureId { get; set; }
public string ArrivalName { get; set; }
}
I want to be able to get the Arrival Name and Departure Name from the option selected when the user submits a form from the Book View.
This is currently the [HttpPost] Book Action Result:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Book(Booking bk)
{
List<Departure> departures = new List<Departure>();
List<Arrival> arrivals = new List<Arrival>();
using (BookingEntities db = new BookingEntities())
{
departures = db.Departures.OrderBy(a => a.DepartureName).ToList();
if (bk != null && bk.DepartureId > 0)
{
arrivals = db.Arrivals.Where(a => a.DepartureId.Equals(bk.DepartureId)).OrderBy(a => a.ArrivalName).ToList();
}
}
ViewBag.DepartureId = new SelectList(departures, "DepartureId", "DepartureName", bk.DepartureId);
ViewBag.ArrivalId = new SelectList(arrivals, "ArrivalId", "ArrivalName", bk.ArrivalId);
string emailTo = bk.BookingEmail;
string emailBody = "Thank you for Booking with Callum Airways, " + bk.BookingFirstname + " " + bk.BookingSurname + ".\n" +
"Here is confirmation that your booking was a success. We have listed the information you entered into this email.\n" +
"Email: " + bk.BookingEmail + ",\n" +
"Mobile Tel: " + bk.BookingMobileTel + ",\n" +
"Home Tel: " + bk.BookingHomeTel + ",\n" +
"Age: " + bk.BookingAge + ",\n" +
"Number of Passengers: " + bk.BookingPassengers + ".\n\n" +
// Departure and Arrival Names
"If you want to review/delete your booking please register an account on ************.";
// All the other booking information.
using (BookingEntities db = new BookingEntities())
{
if (ModelState.IsValid)
{
db.Bookings.Add(bk);
db.SaveChanges();
ModelState.Clear();
bk = null;
MailAddress from = new MailAddress("*************#gmail.com");
MailAddress to = new MailAddress(emailTo);
MailMessage message = new MailMessage(from, to);
message.Subject = "Booking Confirmation - ********";
message.Body = emailBody;
SmtpClient smtp = new SmtpClient();
smtp.Host = "smtp.gmail.com";
smtp.Port = 587;
smtp.UseDefaultCredentials = false;
smtp.Timeout = 10000;
smtp.DeliveryMethod = SmtpDeliveryMethod.Network;
smtp.Credentials = new System.Net.NetworkCredential("*********************", "*******");
smtp.EnableSsl = true;
smtp.Send(message);
}
}
return View("~/Views/Home/Index.cshtml", bk);
}
I've been able to get the value of BookingFirstname and others but because there is just the foreign key not the name in the Booking Class. I'm not sure how I can get ArrivalName and DepartureName.
Arrival arrival = new Arrival();
arrival = db.Arrival.Where(w => w.ArrivalId.Equals(bk.ArrivalId).FirstOrDefault();
ViewBag.ArrivalName = arrival.ArrivalName;
Departure departure = new Departure();
departure = db.Departure.Where(w => w.DeaId.Equals(bk.ArrivalId).FirstOrDefault();
ViewBag.DepartureName = departure.DepartureName;
I'm using multiple mapping for a current query and now I need to map another object on the initial query.
For example:
public class Part {
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address {
public int Id { get; set; }
public string Street { get; set; }
public SiteOu Ou { get; set; }
}
public class SiteOu
public int Id { get; set; }
public string Name { get; set; }
}
Dapper:
connection.Query<Part, Address, Part>(sql, (part, address) => {
part.Address = address;
});
How do I get the Address class to have the SiteOu information?
This example isn't what I'm actually doing because I've actually got
Query<T1,T2,T3,T4,T5,TResult>();
I'm doing 1 select and 5 joins in my query. So hopefully I don't need more overloads of Query.
Dapper allows you to map a single row to multiple objects, so you can just map SiteOu as part of the same query.
[Test]
public void TestSplitOn()
{
var conn = new SqlConnection(#"Data Source=.\SQLEXPRESS;Integrated Security=true;Initial Catalog=db");
conn.Open();
const string sql = "select Id = 1, Name = 'My Part', " +
"Id = 2, Street = 'My Street', " +
"Id = 3, Name = 'My Site'";
var result = conn.Query<Part, Address, SiteOu, Part>(sql, (part, address, siteOu) =>
{
part.Address = address;
address.Ou = siteOu;
return part;
},
commandType: CommandType.Text
).FirstOrDefault();
Assert.That(result, Is.Not.Null);
Assert.That(result.Address, Is.Not.Null);
Assert.That(result.Address.Ou, Is.Not.Null);
}
Important Note: Dapper assumes your Id columns are named "Id" or "id", if your primary key is different or you would like to split the wide row at point other than "Id", use the optional 'splitOn' parameter.
If you have more that 5 types to map, another out of the box option is to use QueryMultiple extension. Here is an example from the Dapper docs.
var sql =
#"
select * from Customers where CustomerId = #id
select * from Orders where CustomerId = #id
select * from Returns where CustomerId = #id";
using (var multi = connection.QueryMultiple(sql, new {id=selectedId}))
{
var customer = multi.Read<Customer>().Single();
var orders = multi.Read<Order>().ToList();
var returns = multi.Read<Return>().ToList();
...
}
Also check out this thread.